CS UBB Timetable Fixer

A userscript that enhances the tabelar timetables for CS UBB students.

La data de 10-03-2018. Vezi ultima versiune.

// ==UserScript==
// @name         CS UBB Timetable Fixer
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  A userscript that enhances the tabelar timetables for CS UBB students.
// @author       myklosbotond
// @match        http://www.cs.ubbcluj.ro/files/orar/*/tabelar/*.html
// @exclude      http://www.cs.ubbcluj.ro/files/orar/*/tabelar/index.html
// @require      https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @resource     jqUI_CSS https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_getResourceText
// ==/UserScript==

/*
 * jshint esversion: 6
 * jshint esnext: true
*/

function getDefaultOptions() {
    'use strict';

    return {
        weekOffset: 0,
        group: 0,
        halfGroup: 0,
        disciplinesToReplace: {},
        disciplinesToHide: [],
        customTable: [],
        fadeHidden: true,
        fadeHalfGroup: true
    };
}

var defaultHTML;
var options;
var disciplines = [];
var idList = new Set();
var customError = false;

const days = ["Luni", "Marti", "Miercuri", "Joi", "Vineri"];

const disciplineColumn = "disciplina";

(function () {
    'use strict';

    loadOptions();
    setupCss();
    setupFunctions();

    structureHTML();
    injectSettingHTML();

    getDisciplinesList();

    setEventHandlers();
    updateSettingsUi();

    runFixer();
})();

function getBaseOptionKey() {
    return "options";
}

function getOptionKey() {
    parts = location.pathname.match('/([0-9]+-[0-9]+)/.*/([^.]+)');
    try {
        return getBaseOptionKey() + "_" + parts[1] + "_" + parts[2];
    }
    catch (err) {
        return getBaseOptionKey();
    }
}

function loadOptions() {
    'use strict';

    let defOptions = getDefaultOptions();
    let savedOptions = GM_getValue(getOptionKey(), GM_getValue(getBaseOptionKey(), "{}"));
    try {
        savedOptions = JSON.parse(savedOptions);
    }
    catch (err) {
        savedOptions = {};
    }

    options = $.extend(defOptions, savedOptions);
}

//color in just the curs / laboratory column on unselected

function setupCss() {
    'use strict';

    GM_addStyle("body {margin: 0;}");
    GM_addStyle("#settings-wrapper {background: #f5f9fb;}");

    GM_addStyle("#settings-container {position: relative;}");
    GM_addStyle("#toggle-settings {position: absolute;right: 1%;top: 12px;}");
    GM_addStyle("#tables-wrapper {margin-bottom: 20px;}");

    GM_addStyle(".curs-tr {background: #fffedd;}");
    GM_addStyle(".curs-tr td a{color: #c16b00;}");
    GM_addStyle(".seminar-tr {background: #ddf2ff;}");
    GM_addStyle(".seminar-tr td a{color: #0e62a5;}");
    GM_addStyle(".labor-tr {background: #e4ffdd;}");
    GM_addStyle(".labor-tr td a{color: #009688;}");

    GM_addStyle('.error {color: red;}');

    GM_addStyle(".last-of-day-tr{border-bottom: 2px solid}");

    GM_addStyle("a:hover, a:hover font {background-color: transparent; text-decoration: underline}");

    GM_addStyle(".other-week-tr {filter: grayscale(100%);}");
    GM_addStyle(".other-week-tr td {background:transparent}");
    GM_addStyle(".other-week-tr:hover {filter: grayscale(0%);}");

    GM_addStyle(".meh-tr {opacity: 0.3; filter: grayscale(100%);}");
    GM_addStyle(".meh-tr td {background:transparent}");
    GM_addStyle(".meh-tr:hover {opacity: 0.7}");
    GM_addStyle(".nope-tr {background: transparent}");
    GM_addStyle(".nope-tr td {background: transparent}");
    GM_addStyle(".nope-tr a {color: #003399}");
    GM_addStyle(".hide-tr {display: none;}");

    let jqUI_CssSrc = GM_getResourceText("jqUI_CSS");
    jqUI_CssSrc = jqUI_CssSrc.replace(/url\(['"]images\/ui\-bg_.*00\.png['"]\)/g, "");
    jqUI_CssSrc = jqUI_CssSrc.replace(/url\(['"]images\/ui\-icons_.*\.png['"]\)/g, "");

    GM_addStyle(jqUI_CssSrc);

    GM_addStyle(".ui-selectmenu-button.ui-button {width: auto}");
    GM_addStyle(".ui-front:not(.ui-widget-overlay):not(.ui-selectmenu-open){z-index:100 !important; } .ui-selectmenu-open {z-index:9999 !important;}");

    GM_addStyle(".ui-selectmenu-icon.ui-icon {float: right; margin-top: 8px; border-style: solid; border-width: 3px 3px 0 3px; border-color: #0f0f0d transparent transparent transparent; width: 0; height: 0;}");

    GM_addStyle('span.ui-button-icon.ui-icon.ui-icon-closethick {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg\" width="24" height="24" viewBox="0 0 24 24"><path d="M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z"/></svg>\'); background-position: center; background-size: 50%;}');

    GM_addStyle('#hider-list-wrapper {max-height: 300px; overflow: auto; margin-bottom: 9px;}');
    GM_addStyle('#renamer-list-wrapper {max-height: 330px; overflow: auto; margin-bottom: 9px;}');

    GM_addStyle('#hider-dialog table{ width: 100%; }');
    GM_addStyle('#hider-dialog .check-column{ width: 1px; }');

    GM_addStyle('.option-wrapper{ display: inline-block; margin-top: 7px;}');
    GM_addStyle('.finder-options-wrapper{     margin-left: 91px;}');

    GM_addStyle('#finder-result, #finder-time-result { margin: 10px auto 0 auto; }');

    GM_addStyle('#import-status { color:red; font-weight: bold; }');

    GM_addStyle('.beta-button::after {content: "BETA"; font-size: 10px; vertical-align: super; color: white; background: darkred; font-weight: bold; padding: 0 2px; border-radius: 3px; margin-left: 3px;}');

    /* ================ Custom ============== */

    GM_addStyle('#custom-empty-view { text-align: center; }');
    GM_addStyle('#custom-selected-wrapper, #custom-list-wrapper { max-height: 500px; overflow: auto; display: inline-block;}');
    GM_addStyle('#custom-controls { display: inline-block; vertical-align: top; margin: 100px 10px 0;}');

    GM_addStyle('#custom-work-view .column-disciplina {max-width: 40px; overflow: hidden; white-space: nowrap;}');

    let tdSel0 = 'td:not(:first-child):not(:nth-child(2)):not(:last-child)';

    GM_addStyle('#custom-work-view .ui-selectee:not(.ui-selected) {background:none;}');
    GM_addStyle('#custom-work-view .ui-selectee:not(.ui-selected) td {opacity: .8;}');
    GM_addStyle('#custom-work-view .ui-selectee:not(.ui-selected) a {color: black;}');
    GM_addStyle('#custom-work-view .curs-tr.ui-selectee ' + tdSel0 + ' {background:#fffedd;}');
    GM_addStyle('#custom-work-view .seminar-tr.ui-selectee ' + tdSel0 + ' {background:#ddf2ff;}');
    GM_addStyle('#custom-work-view .labor-tr.ui-selectee ' + tdSel0 + ' {background:#e4ffdd;}');

    /* ================ Icons =============== */
    GM_addStyle('.ui-icon-gear {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"/></svg>\'); background-position: center; background-size: 100%;}');

    GM_addStyle('.ui-icon-hide {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.604 2.562l-3.346 3.137c-1.27-.428-2.686-.699-4.243-.699-7.569 0-12.015 6.551-12.015 6.551s1.928 2.951 5.146 5.138l-2.911 2.909 1.414 1.414 17.37-17.035-1.415-1.415zm-6.016 5.779c-3.288-1.453-6.681 1.908-5.265 5.206l-1.726 1.707c-1.814-1.16-3.225-2.65-4.06-3.66 1.493-1.648 4.817-4.594 9.478-4.594.927 0 1.796.119 2.61.315l-1.037 1.026zm-2.883 7.431l5.09-4.993c1.017 3.111-2.003 6.067-5.09 4.993zm13.295-4.221s-4.252 7.449-11.985 7.449c-1.379 0-2.662-.291-3.851-.737l1.614-1.583c.715.193 1.458.32 2.237.32 4.791 0 8.104-3.527 9.504-5.364-.729-.822-1.956-1.99-3.587-2.952l1.489-1.46c2.982 1.9 4.579 4.327 4.579 4.327z"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-rename {background-image: url(\'data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="m18.483 14.133h-1.462v-9h1.462v9z"/><path d="m12.525 6.6228h-10.542v6h10.542v1.51h-12.042v-9h12.042v1.49z"/><path d="m15.521 2.8728v13.5c0 0.414 0.335 0.75 0.75 0.75h1.5v1.5h-6v-1.5h1.504c0.415 0 0.75-0.336 0.75-0.75v-13.5c0-0.414-0.335-0.75-0.75-0.75h-1.504v-1.499h6v1.499h-1.5c-0.415 0-0.75 0.336-0.75 0.75"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-find {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M21.172 24l-7.387-7.387c-1.388.874-3.024 1.387-4.785 1.387-4.971 0-9-4.029-9-9s4.029-9 9-9 9 4.029 9 9c0 1.761-.514 3.398-1.387 4.785l7.387 7.387-2.828 2.828zm-12.172-8c3.859 0 7-3.14 7-7s-3.141-7-7-7-7 3.14-7 7 3.141 7 7 7z"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-reset {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17.026 22.957c10.957-11.421-2.326-20.865-10.384-13.309l2.464 2.352h-9.106v-8.947l2.232 2.229c14.794-13.203 31.51 7.051 14.794 17.675z"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-printer {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14 20h-6v-1h6v1zm10-15v13h-4v6h-16v-6h-4v-13h4v-5h16v5h4zm-6 10h-12v7h12v-7zm0-13h-12v3h12v-3zm4 5.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5.224.5.5.5.5-.224.5-.5zm-6 9.5h-8v1h8v-1z"/></svg>\'); background-position: center; background-size: 100%;}');

    GM_addStyle('.ui-icon-import {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 12l9-8v6h15v4h-15v6z"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-export {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 12l-9-8v6h-15v4h15v6z"/></svg>\'); background-position: center; background-size: 100%;}');
    GM_addStyle('.ui-icon-customize {background-image: url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M14.078 4.232l-12.64 12.639-1.438 7.129 7.127-1.438 12.641-12.64-5.69-5.69zm-10.369 14.893l-.85-.85 11.141-11.125.849.849-11.14 11.126zm2.008 2.008l-.85-.85 11.141-11.125.85.85-11.141 11.125zm18.283-15.444l-2.816 2.818-5.691-5.691 2.816-2.816 5.691 5.689z"/></svg>\'); background-position: center; background-size: 100%;}');
    //GM_addStyle('.ui-icon-<> {background-image: url(\'data:image/svg+xml;utf8, <svg here>  \'); background-position: center; background-size: 100%;}');

    /* =========== Printer styles =========== */

    GM_addStyle('@media print{ #settings-wrapper {display:none;} #tables-wrapper table{page-break-after: always;}}');

}

function setupFunctions() {
    'use strict';

    Date.prototype.getWeek = function () {
        var onejan = new Date(this.getFullYear(), 0, 1);
        return Math.ceil((((this - onejan) / 86400000) + onejan.getDay() + 1) / 7);
    };

    /**
     * @returns {day, hour, frequency, room, group, type, discipline, teacher}
     */
    jQuery.fn.getColumns = function () {
        if (this.is('tr')) {
            return {
                day: this.find('.column-ziua'),
                hour: this.find('.column-orele'),
                frequency: this.find('.column-frecventa'),
                room: this.find('.column-sala'),
                group: this.find('.column-formatia'),
                type: this.find('.column-tipul'),
                discipline: this.find('.column-disciplina'),
                teacher: this.find('.column-cadrul-didactic')
            };
        }
        else {
            return {};
        }
    };

    setDialogsPositionFixed();
}

function removeElement(array, element) {
    const dataIdx = array.indexOf(element);
    if (dataIdx >= 0) {
        array.splice(dataIdx, 1);
    }
}

function setDialogsPositionFixed() {
    $.ui.dialog.prototype._oldinit = $.ui.dialog.prototype._init;
    $.ui.dialog.prototype._init = function () {

        $(this.element).parent().css("position", "fixed");
        $(this.element).parent().addClass("no-select");
        $(this.element).parent().css("margin", "5px"); //limit 5px to the top
        $(this.element).dialog("option", {
            resizeStart: function (event, ui) {
                $(event.target).parent().css("position", "fixed");
                return true;
            },
            resizeStop: function (event, ui) {
                $(event.target).parent().css("position", "fixed");
                return true;
            }
        });
        this._oldinit();
    };
}

function structureHTML() {
    'use strict';

    let html = $('center').html();
    $('center').html('<div id="tables-wrapper"></div>');
    let wrapper = $('#tables-wrapper');
    wrapper.html(html);

    wrapper
        .find('h1:first')
        .insertBefore(wrapper)
        .attr('id', 'title-heading');

    wrapper.find('table').each(function () {
        let columns = [];
        let grp = $(this).prev('h1').text().replace('Grupa ', '');

        $(this).addClass('table-group-' + grp);

        $(this).find('tr:first th').each(function () {
            columns.push($(this).text().replace(/\s/g, '-').toLowerCase());
        });

        for (let i = 0; i < columns.length; ++i) {
            $(this).find('td:nth-child(' + (i + 1) + ')').addClass('column-' + columns[i]);
        }
    });

    $('.column-' + disciplineColumn).each(function () {
        $(this).attr('data-original', $(this).text());
    });

    wrapper.find("tr:contains('Curs')").addClass("curs-tr");
    wrapper.find("tr:contains('Seminar')").addClass("seminar-tr");
    wrapper.find("tr:contains('Laborator')").addClass("labor-tr");

    wrapper.find('tr:has(td)').each(setRowId);

    defaultHTML = wrapper.html();
}

function setRowId() {
    let row = $(this);

    columns = row.getColumns();

    let day = columns.day.text();
    let hour = columns.hour.text();
    let freq = columns.frequency.text();
    let grp = columns.group.text();
    let disc = columns.discipline.attr('data-original');

    let id = '';
    let dayi = -1;

    switch (day.substr(0, 2)) {
        case 'Lu':
            dayi = 0;
            break;
        case 'Ma':
            dayi = 1;
            break;
        case 'Mi':
            dayi = 2;
            break;
        case 'Jo':
            dayi = 3;
            break;
        case 'Vi':
            dayi = 4;
            break;
    }

    id += dayi;

    let hr = hour.split('-')[0];
    if (hr.length < 2) {
        hr = '0' + hr;
    }
    id += hr;
    if (/sapt\.\s[0-9]/.test(freq)) {
        id += 's' + freq.replace('sapt. ', '');
    }
    else {
        id += 's0';
    }
    id += grp;
    id += disc.replace(/\s/g, '-');

    idList.add(id);
    row.attr("data-id", id);
}

function injectSettingHTML() {
    'use strict';

    let groupOptions = '';

    let html = '<div id="settings-container">' +
        '<button id="toggle-settings">Settings</button>' +
        '<div id="settings-wrapper">' +
        '   <div id="week-wrapper" class="option-wrapper">' +
        '       <span id="week-span"></span>' +
        '       <label for="offset-week">Offset week by 1</label>' +
        '       <input type="checkbox" id="offset-week" />' +
        '   </div>' +
        '   <div id="group-wrapper" class="option-wrapper">' +
        '       <label for="group-select">Your group: </label>' +
        '       <select id="group-select">' +
        '       <option value="0"></option>';
    $('#tables-wrapper h1').each(function () {
        try {
            let group = parseInt($(this).text().replace('Grupa ', ''), 10);
            let sel = '';
            if (group == options.group) {
                sel = 'selected';
            }
            groupOptions += '<option value="' + group + '" ' + sel + '>' + group + '</option>';
        }
        catch (e) {
            console.log(e);
        }
    });
    html += groupOptions;
    html += '</select>' +
        '   </div>' +
        '   <div id="half-group-wrapper" class="option-wrapper">' +
        '       <label for="half-group-select">Your half group: </label>' +
        '       <select id="half-group-select">' +
        '          <option value="1">1</option>' +
        '          <option value="2">2</option>' +
        '          <option value="0">Don\'t hide</option>' +
        '       </select>' +
        '   </div>' +
        '   <div id="hidden-fade-wrapper" class="option-wrapper">' +
        '       <label for="hidden-fade-select">Hidden items: </label>' +
        '       <select id="hidden-fade-select">' +
        '          <option value="true">Faded</option>' +
        '          <option value="false">Hidden</option>' +
        '       </select>' +
        '   </div>' +
        '   <div id="other-halfgr-fade-wrapper" class="option-wrapper">' +
        '       <label for="other-halfgr-fade-select">Other halfgroup\'s items: </label>' +
        '       <select id="other-halfgr-fade-select">' +
        '          <option value="true">Faded</option>' +
        '          <option value="false">Hidden</option>' +
        '       </select>' +
        '   </div>';

    html += '<br>' +
        '   <div id="buttons-wrapper" class="option-wrapper">' +
        '       <button id="hider-button">Hidden disciplines</button>' +
        '       <button id="renamer-button">Rename disciplines</button>' +
        '       <button id="finder-button">Find disciplines</button>' +
        '       <button id="reset-button">Reset Settings</button>' +
        '       <button id="print-button">Print this page</button>' +
        '   </div>';

    html += '<br>' +
        '   <div id="buttons-wrapper-2" class="option-wrapper">' +
        '       <button id="import-button">Import settings</button>' +
        '       <button id="export-button">Export settings</button>' +
        '       <a id="download-helper" class="ui-helper-hidden"></a>' +
        '       <button id="customize-button" class="beta-button">Edit custom</button>' +
        '   </div><br>';

    html += '<div id="hider-dialog">' +
        '       <span>Select the disciplines you <b>don\'t</b> want highlighted:</span>' +
        '       <div id="hider-list-wrapper"></div>' +
        '       <div style="float: right;">' +
        '           <button id="hider-save">Save</button>' +
        '           <button id="hider-close">Cancel</button>' +
        '       </div>' +
        '   </div>' +

        '   <div id="renamer-dialog">' +
        '       <span>Enter the text you want each discipline to be renamed to. <br> Leave the field blank if you don\'t want a discipline\'s name replaced.</span>' +
        '       <div id="renamer-list-wrapper"></div>' +
        '       <div style="float: right;">' +
        '           <button id="renamer-save">Save</button>' +
        '           <button id="renamer-close">Cancel</button>' +
        '       </div>' +
        '   </div>' +

        '   <div id="finder-dialog">' +
        '       <div id="tabs">' +
        '           <ul>' +
        '               <li><a href="#tabs-1">By Discipline</a></li>' +
        '               <li><a href="#tabs-2">By Time</a></li>' +
        '           </ul>' +
        '           <div id="tabs-1">' +
        '               <div class="finder-options-wrapper">' +
        '                   <label for="finder-select">Discipline to search for: </label><select id="finder-select"></select>' +
        '                   <label for="finder-type"> Type: </label>' +
        '                   <select id="finder-type">' +
        '                       <option value="curs">Course</option>' +
        '                       <option value="seminar">Seminar</option>' +
        '                       <option value="laborator">Laboratory</option>' +
        '                   </select>' +
        '               </div>' +
        '               <table id="finder-result"></table>' +
        '           </div>' +
        '           <div id="tabs-2">' +
        '               <div class="finder-options-wrapper">' +
        '                   <label for="finder-day">Day: </label><select id="finder-day"></select>' +
        '                   <label for="finder-hour"> Hour: </label>' +
        '                   <select id="finder-hour"></select>' +
        '                   <label for="finder-sapt"> Week: </label>' +
        '                   <select id="finder-sapt">' +
        '                       <option value="0">Both</option>' +
        '                       <option value="1">1</option>' +
        '                       <option value="2">2</option>' +
        '                   </select>' +
        '               </div>' +
        '               <table id="finder-time-result"></table>' +
        '           </div>' +
        '       </div>' +
        '   </div>' +

        '   <div id="import-dialog">' +
        '       <span id="import-status"></span>' +
        '       <label for="import-file">Choose settings file:</label>' +
        '       <input type="file" id="import-file" accept=".json,text/json"/>' +
        '       <div style="float: right; margin-top: 20px;">' +
        '           <button id="do-import">Import</button>' +
        '       </div>' +
        '   </div>' +

        '   <div id="customize-dialog">' +
        '       <div id="custom-empty-view">' +
        '           <label for="custom-group-select">List empty. Start with group: </label>' +
        '           <select id="custom-group-select">' +
        groupOptions +
        '           </select>' +
        '           <br>' +
        '           <button id="custom-start">Go</button>' +
        '       </div>' +
        '       <div id="custom-work-view">' +
        '           <div id="custom-selected-wrapper">' +
        '               <table id="custom-selected-table">' +
        '               </table>' +
        '           </div>' +
        '           <div id="custom-controls">' +
        '               <button id="custom-add" title="Add">&lt;&lt;</button>' +
        '               <hr>' +
        '               <button id="custom-remove" title="Remove">&gt;&gt;</button>' +
        '           </div>' +
        '           <div id="custom-list-wrapper">' +
        '               <table id="custom-list-table">' +
        '               </table>' +
        '           </div>' +
        '           <hr>' +
        '           <button id="custom-clear" style="margin-left: 42%;">Clear All</button>' +
        '       </div>' +
        '   </div>';

    html += '</div></div>';
    $(html).insertAfter('#title-heading');

    for (let i = 0; i < days.length; ++i) {
        $('#finder-day').append('<option value="' + days[i] + '">' + days[i] + '</option>');
    }

    $('#offset-week').checkboxradio({
        icon: false
    });
    $('button').button();
    $('select').selectmenu();

    $('#toggle-settings').button({
        icon: 'ui-icon-gear',
        text: false
    });
    $('#hider-button').button({
        icon: 'ui-icon-hide'
    });
    $('#renamer-button').button({
        icon: 'ui-icon-rename'
    });
    $('#finder-button').button({
        icon: 'ui-icon-find'
    });
    $('#reset-button').button({
        icon: 'ui-icon-reset'
    });
    $('#print-button').button({
        icon: 'ui-icon-printer'
    });
    $('#import-button').button({
        icon: 'ui-icon-import'
    });
    $('#export-button').button({
        icon: 'ui-icon-export'
    });
    $('#customize-button').button({
        icon: 'ui-icon-customize'
    });

    $('#tabs').tabs();

    $('#hider-dialog').dialog({
        modal: true,
        width: 320,
        title: 'Hide disciplines',
        autoOpen: false
    });

    $('#renamer-dialog').dialog({
        modal: true,
        width: 420,
        title: 'Rename disciplines',
        autoOpen: false
    });

    $('#finder-dialog').dialog({
        modal: true,
        width: 620,
        height: 'auto',
        title: 'Find disciplines',
        autoOpen: false
    });
    updateFinderHourSelect();

    $('#import-dialog').dialog({
        modal: true,
        width: 320,
        height: 'auto',
        title: 'Import settings',
        autoOpen: false
    });
    $('#do-import').button("option", "disabled", true);

    $('#customize-dialog').dialog({
        modal: true,
        width: 800,
        height: 'auto',
        title: 'Edit your custom timetable',
        autoOpen: false
    });

    $('#custom-add, #custom-remove').button('disable');

    $('#custom-selected-table').selectable({
        filter: 'tr',
        selected: function () {
            $('#custom-add').button('disable');
            $('#custom-remove').button('enable');

            $('#custom-list-table .ui-selected').removeClass('ui-selected');
        },
        unselected: function () {
            if ($(this).find('.ui-selected').length == 0) {
                $('#custom-remove').button('disable');
            }
        }
    });
    $('#custom-list-table').selectable({
        filter: 'tr',
        selected: function () {
            $('#custom-add').button('enable');
            $('#custom-remove').button('disable');

            $('#custom-selected-table .ui-selected').removeClass('ui-selected');
        },
        unselected: function () {
            if ($(this).find('.ui-selected').length == 0) {
                $('#custom-add').button('disable');
            }
        }
    });

}

function getDisciplinesList() {
    let discSet = new Set();
    $('.column-' + disciplineColumn).each(function () {
        discSet.add($(this).text());
    });

    disciplines = [...discSet];
}

function setEventHandlers() {
    $('body')
        .on('click', '#toggle-settings', function () {
            $('#settings-wrapper').slideToggle();
        })
        .on('change', '#offset-week', function () {
            options.weekOffset = ($(this).prop('checked') ? 1 : 0);
            performChange();
        })
        .on('selectmenuchange', '#group-select', function () {
            options.group = parseInt($(this).val(), 10);
            performChange();
        })
        .on('selectmenuchange', '#half-group-select', function () {
            options.halfGroup = parseInt($(this).val(), 10);
            performChange();
        })
        .on('selectmenuchange', '#hidden-fade-select', function () {
            let value = $(this).val() === 'true';
            options.fadeHidden = value;
            performChange();
        })
        .on('selectmenuchange', '#other-halfgr-fade-select', function () {
            let value = $(this).val() === 'true';
            options.fadeHalfGroup = value;
            performChange();
        })
        .on('click', '#hider-button', function () {
            $('#hider-dialog')
                .dialog('option', 'position', {
                    my: 'center top',
                    at: 'center top+50',
                    of: window
                })
                .dialog('open');
        })
        .on('click', '#renamer-button', function () {
            $('#renamer-dialog')
                .dialog('option', 'position', {
                    my: 'center top',
                    at: 'center top+50',
                    of: window
                })
                .dialog('open');
        })
        .on('click', '#reset-button', function () {
            if (confirm('Are you sure you want to reset?')) {
                options = getDefaultOptions();
                performChange();
            }
        })
        .on('click', '#import-button', function () {
            $('#import-dialog')
                .dialog('option', 'position', {
                    my: 'center top',
                    at: 'center top+50',
                    of: window
                })
                .dialog('open');
        })
        .on('click', '#export-button', exportOptions)
        .on('click', '#print-button', window.print)
        .on('click', '#hider-save', function () {
            let hides = [];
            $('#hider-list-wrapper tr').each(function () {
                if ($(this).find('.hide-check').is(':checked')) {
                    hides.push($(this).find('.discipline-name').attr('data-discipline'));
                }
            });

            $('#hider-dialog').dialog('close');
            options.disciplinesToHide = hides;
            performChange();
        })
        .on('click', '#hider-close', function () {
            $('#hider-dialog').dialog('close');
        })
        .on('click', '#renamer-save', function () {
            let renames = {};
            $('#renamer-list-wrapper tr').each(function () {
                let renameTo = $(this).find('.rename-to').val();
                if (renameTo.length > 0) {
                    renames[$(this).find('.discipline-name').text()] = renameTo;
                }
            });

            options.disciplinesToReplace = renames;

            $('#renamer-dialog').dialog('close');
            performChange();
        })
        .on('click', '#renamer-close', function () {
            $('#renamer-dialog').dialog('close');
        })
        .on('click', '#finder-button', function () {
            $('#finder-dialog')
                .dialog('option', 'position', {
                    my: 'center top',
                    at: 'center top+50',
                    of: window
                })
                .dialog('open');
            findDiscipline();
        })
        .on('selectmenuchange', '#finder-dialog select', findDiscipline)
        .on('selectmenuchange', '#finder-day', updateFinderHourSelect)
        .on('selectmenuchange', '#finder-hour, #finder-sapt', findDisciplineByTime)

        .on('change', '#import-file', function () {
            let files = $(this)[0].files;
            $('#do-import').button("option", "disabled", files.length < 1);
        })
        .on('click', '#do-import', importOptions)
        .on('click', '#customize-button', function () {
            $('#customize-dialog')
                .dialog('option', 'position', {
                    my: 'center top',
                    at: 'center top+50',
                    of: window
                })
                .dialog('open');
        })
        .on('click', '#custom-start', function () {
            let grp = $('#custom-group-select').val();
            let table = $('.table-group-' + grp);

            table.find('tr:has(td)').each(function () {
                let rowId = $(this).attr('data-id');
                if (rowId && rowId.length) {
                    options.customTable.push(rowId);
                }
            });
            performChange();
        })
        .on('click', '#custom-clear', function () {
            if (confirm("Are you sure you want to clear everything?")) {
                options.customTable = [];
                performChange();
            }
        })
        .on('click', '#custom-add', function () {
            let selected = [];
            $('#custom-list-table .ui-selected').each(function () {
                options.customTable.push($(this).attr('data-id'));
            });
            sortIds(options.customTable);
            performChange();
            $('#custom-add').button('disable');
        })
        .on('click', '#custom-remove', function () {
            let selected = [];
            $('#custom-selected-table .ui-selected').each(function () {
                removeElement(options.customTable, $(this).attr('data-id'));
            });
            performChange();
            $('#custom-remove').button('disable');
        });
}

function exportOptions() {
    let dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(options));
    let downloadHelper = $('#download-helper')

    downloadHelper.attr('href', dataStr);
    downloadHelper.attr('download', 'fixer-options.json');
    downloadHelper[0].click();
}

function importOptions() {
    let files = $('#import-file')[0].files;
    if (files.length > 0) {
        let file = files[0];
        let reader = new FileReader();

        reader.onload = function (e) {
            parseOptionFile(reader.result);
        }

        reader.readAsText(file);
    }
}

function parseOptionFile(text) {
    try {
        options = JSON.parse(text);
        performChange();
        $('#import-dialog').dialog('close');
        $('#import-status').text("");
    }
    catch (err) {
        $('#import-status').text("Import failed. Invalid JSON.");
        console.error(err);
    }
}

function findDiscipline() {
    let disc = $('#finder-select').val();
    let type = $('#finder-type').val();

    let result = $('#finder-result');
    result.html('');

    $('#tables-wrapper tr').each(function () {
        if ($(this).find('.column-tipul').text().toLowerCase() === type &&
            ($(this).find('.column-disciplina').text() === disc ||
                $(this).find('.column-disciplina').attr('data-original') === disc)) {

            cloneStripped($(this)).appendTo(result);
        }
    });

    let rows = $('#finder-result tr').get();
    rows.sort(function (a, b) {
        a = $(a);
        b = $(b);

        let aDay = a.find('.column-ziua').text();
        let bDay = b.find('.column-ziua').text();
        if (aDay === bDay) {
            let aHr = a.find('.column-orele').text().split('-')[0];
            aHr = parseInt(aHr, 10);
            let bHr = b.find('.column-orele').text().split('-')[0];
            bHr = parseInt(bHr, 10);

            return aHr - bHr;
        }
        else {
            aDay = days.indexOf(aDay);
            bDay = days.indexOf(bDay);
            return aDay - bDay;
        }
    });


    rows = rows.filter(function (value, index, self) {
        if (index + 1 < self.length) {
            let tr = $(value);
            let nextTr = $(self[index + 1]);
            return !equalTrs(tr, nextTr);
        }
        return true;
    });
    $('#finder-result').html(rows);

    addDayLines($('#finder-result'));
}

function equalTrs(a, b) {
    let aDay = a.find('.column-ziua').text();
    let aHr = a.find('.column-orele').text();
    let aFreq = a.find('.column-frecventa').text();
    let aSala = a.find('.column-sala').text();
    let aForm = a.find('.column-formatia').text();
    let aType = a.find('.column-tipul').text();
    let aDisc = a.find('.column-disciplina').text();
    let aTeacher = a.find('.column-cadrul-didactic').text();

    let bDay = b.find('.column-ziua').text();
    let bHr = b.find('.column-orele').text();
    let bFreq = b.find('.column-frecventa').text();
    let bSala = b.find('.column-sala').text();
    let bForm = b.find('.column-formatia').text();
    let bType = b.find('.column-tipul').text();
    let bDisc = b.find('.column-disciplina').text();
    let bTeacher = b.find('.column-cadrul-didactic').text();

    return aDay === bDay && aHr === bHr && aFreq === bFreq && aSala === bSala &&
        aForm === bForm && aType === bType && aDisc === bDisc && aTeacher === bTeacher;
}

function updateFinderHourSelect() {
    let hours = new Set();
    $('tr:contains(' + $('#finder-day').val() + ')').each(function () {
        hours.add($(this).find('.column-orele').text());
    });
    hours = [...hours].sort(function (a, b) {
        a = parseInt(a.split('-')[0], 10);
        b = parseInt(b.split('-')[0], 10);
        return a - b;
    });

    $('#finder-hour').html('');
    for (let i = 0; i < hours.length; ++i) {
        $('#finder-hour').append('<option value="' + hours[i] + '">' + hours[i] + '</option>');
    }
    $('#finder-hour').selectmenu('refresh');

    findDisciplineByTime();
}

function findDisciplineByTime() {
    let day = $('#finder-day').val();
    let hour = $('#finder-hour').val();
    let week = $('#finder-sapt').val();

    let result = $('#finder-time-result');
    result.html('');

    $('#tables-wrapper tr').each(function () {
        let weekString = $(this).find('.column-frecventa').text();
        if ($(this).find('.column-ziua').text().toLowerCase() === day.toLowerCase() &&
            $(this).find('.column-orele').text() === hour &&
            (week == "0" || weekString.replace(/\s/g, '') === '' || weekString === 'sapt. ' + week)) {
            $(this)
                .clone()
                .removeClass('meh-tr')
                .removeClass('other-week-tr')
                .removeClass('nope-tr')
                .removeClass('last-of-day-tr')
                .appendTo(result);
        }
    });

    let rows = $('#finder-time-result tr').get();
    rows.sort(function (a, b) {
        a = $(a);
        b = $(b);

        let aGroup = a.find('.column-formatia').text();
        let bGroup = b.find('.column-formatia').text();
        if (aGroup === bGroup) {
            let aDisc = a.find('.column-disciplina').text();
            let bDisc = b.find('.column-disciplina').text();

            return compareStrings(aDisc, bDisc);
        }
        else {
            return compareStrings(aGroup, bGroup);
        }
    });
    rows = rows.filter(function (value, index, self) {
        if (index + 1 < self.length) {
            let tr = $(value);
            let nextTr = $(self[index + 1]);
            return !equalTrs(tr, nextTr);
        }
        return true;
    });
    $('#finder-time-result').html(rows);
}

function compareStrings(a, b) {
    if (a < b) {
        return -1;
    }
    else if (a > b) {
        return 1;
    }
    return 0;
}

function performChange() {
    saveOptions();
    updateSettingsUi();
    runFixer();
}

function saveOptions() {
    GM_setValue(getOptionKey(), JSON.stringify(options));
}

function updateSettingsUi() {
    let week = getWeekNo();
    if (week === 0) {
        week = 2;
    }
    $('#week-span').text("Week " + week + ": ");

    $('#offset-week')
        .prop('checked', options.weekOffset === 1)
        .checkboxradio('refresh');

    if ($('#tables-wrapper .column-frecventa:contains(sapt)').length === 0) {
        $('#week-wrapper').hide();
    }
    else {
        $('#week-wrapper').show();
    }



    if ($('#group-select:contains(' + options.group + ')').length) {
        $('#group-select')
            .val(options.group)
            .selectmenu('refresh');
    }

    if ($('#tables-wrapper table').length === 1) {
        $('#group-wrapper').hide();
    }
    else {
        $('#group-wrapper').show();
    }

    $('#half-group-select')
        .val(options.halfGroup)
        .selectmenu('refresh');

    $('#hidden-fade-select')
        .val(options.fadeHidden ? 'true' : 'false')
        .selectmenu('refresh');

    $('#other-halfgr-fade-select')
        .val(options.fadeHalfGroup ? 'true' : 'false')
        .selectmenu('refresh');

    if ($('#tables-wrapper .column-formatia:contains(/1)').length === 0 &&
        $('#tables-wrapper .column-formatia:contains(/2)').length === 0) {
        $('#half-group-wrapper').hide();
    }
    else {
        $('#half-group-wrapper').show();
    }

    let hiderList = '<table>';
    let renamerList = '<table>';
    let finderOptions = '';

    for (let i = 0; i < disciplines.length; ++i) {
        let discipline = disciplines[i];

        let discName = discipline;
        let discReplacer = '';
        if (options.disciplinesToReplace.hasOwnProperty(discipline) && options.disciplinesToReplace[discipline]) {
            discName = options.disciplinesToReplace[discipline];
            discReplacer = discName;
        }

        hiderList += '<tr><td class="discipline-name" data-discipline="' + discipline + '">' + discName + '</td><td class="check-column"><input class="hide-check" type="checkbox" ' + (options.disciplinesToHide.includes(discipline) ? ' checked' : '') + '/></td></tr>';
        renamerList += '<tr><td class="discipline-name" >' + discipline + '</td><td class="input-column"><input class="rename-to" type="text" value="' + discReplacer + '"/></td></tr>';
        finderOptions += '<option value="' + discipline + '">' + discName + '</option>';
    }
    hiderList += '</table>';
    renamerList += '</table>';

    $('#hider-list-wrapper').html(hiderList);
    $('#renamer-list-wrapper').html(renamerList);
    $('#finder-select').html(finderOptions);
    $('#finder-select').selectmenu('refresh');

    if (options.customTable.length == 0) {
        $('#custom-empty-view').show();
        $('#custom-work-view').hide();
    } else {
        $('#custom-empty-view').hide();
        $('#custom-work-view').show();
        updateCustomWorkView();
    }
}

function updateCustomWorkView() {
    let chosen = $('#custom-selected-table');
    chosen.html('');

    let remList = $('#custom-list-table');
    remList.html('');


    let remaining = new Set(idList);

    for (let i = 0; i < options.customTable.length; ++i) {
        let id = options.customTable[i];

        remaining.delete(id);

        let row = cloneStripped($('tr[data-id="' + id + '"]').eq(0));
        shortenInfo(row);
        chosen.append(row);
    }
    chosen.selectable('refresh');
    addDayLinesCustom(chosen);

    remaining = Array.from(remaining);
    sortIds(remaining);

    for (let i = 0; i < remaining.length; ++i) {
        let id = remaining[i];

        let row = cloneStripped($('tr[data-id="' + id + '"]').eq(0));
        shortenInfo(row);
        remList.append(row);
    }
    remList.selectable('refresh');
    addDayLinesCustom(remList);
}

function sortIds(array) {
    array.sort();
}

function cloneStripped(element) {
    return element
        .clone()
        .removeClass('meh-tr')
        .removeClass('hide-tr')
        .removeClass('other-week-tr')
        .removeClass('nope-tr')
        .removeClass('last-of-day-tr');
}

function shortenInfo(row) {
    if (row.length == 0) {
        return;
    }
    let columns = row.getColumns();
    /*
     * @returns {day, 
     * hour, 
     * frequency, room, group, type, discipline, teacher}
     */

    let day = columns.day.text();
    columns.day.text(day.substr(0, 2)).attr('title', day);

    let hour = columns.hour.text();
    columns.hour.text(hour.split('-')[0]).attr('title', hour);

    let sapt = columns.frequency.text();
    columns.frequency.text(sapt.replace('sapt. ', 's')).attr('title', sapt);

    let room = columns.room.text();
    columns.room.text(room.substr(0, 4) + ((room.length > 4) ? '.' : '')).attr('title', room);

    let type = columns.type.text();
    columns.type.text(type.substr(0, 1)).attr('title', type);

    let disc = columns.discipline.text();
    columns.discipline.attr('title', disc);

    let teacher = columns.teacher.text();
    let teach = teacher.split(' ');
    let tStr = teach[1];

    let tt = tStr.split('-');
    tStr = tt[0].substr(0, 1) + tt[0].substr(1).toLowerCase();

    for (let i = 1; i < tt.length; ++i) {
        tStr += '-' + tt[i].substr(0, 1) + '.';
    }

    for (let i = 2; i < teach.length; ++i) {
        tStr += ' ' + teach[i].substr(0, 1) + '.';
    }
    columns.teacher.text(tStr).attr('title', teacher);
}

function runFixer() {
    'use strict';

    $('#tables-wrapper').html(defaultHTML);

    let myGroupHeading = $("h1:contains('" + options.group + "')");
    if ($('#tables-wrapper table').length === 1) {
        myGroupHeading = $('#tables-wrapper h1');
    }
    let myGroupTable = myGroupHeading.next("table");

    myGroupHeading.prependTo("#tables-wrapper").css("margin", "30px");
    myGroupTable.insertAfter(myGroupHeading).css("margin-bottom", "147px");

    let hasCustom = options.customTable.length > 0;

    if (hasCustom) {
        if (!insertCustomTable()) {
            options.customTable = [];
            customError = true;
            performChange();
            return;
        }
        else {
            customError = false;
        }
    }
    if (customError) {
        $('#tables-wrapper').prepend('<hgroup><h1 class="error">Some custom items were not found</h1><h2 class="error">The timetable might have changed. The custom table has been cleared.</h2></hgroup>');
    }
    let customTable = $('.table-custom');

    for (let discipline in options.disciplinesToReplace) {
        if (options.disciplinesToReplace.hasOwnProperty(discipline) && options.disciplinesToReplace[discipline]) {
            let a = $('.column-' + disciplineColumn + ' a:contains(' + discipline + ')');
            a.each(function () {
                let text = $(this).text();
                $(this)
                    .text(text.replace(discipline, options.disciplinesToReplace[discipline]));
            });
        }
    }

    fixTable(myGroupTable);
    if (hasCustom) {
        fixTable(customTable, true);
    }


    $("table").each(function () {
        addDayLines($(this));
    });
}

function fixTable(table, custom = false) {

    for (let i = 0; i < options.disciplinesToHide.length; ++i) {
        let discipline = options.disciplinesToHide[i];
        let row = table
            .find('.column-' + disciplineColumn + '[data-original="' + discipline + '"]')
            .closest('tr');

        if (options.fadeHidden) {
            row.addClass("meh-tr")
                .addClass("nope-tr");
        }
        else {
            row.addClass("hide-tr");
        }
    }

    if (!custom) {
        let otherHgTrs = table.find("tr:contains('" + options.group + "/" + (3 - options.halfGroup) + "')");

        if (options.fadeHalfGroup) {
            otherHgTrs.addClass("meh-tr");
        }
        else {
            otherHgTrs.addClass("hide-tr");
        }
    }

    let weekNo = getWeekNo();
    let otherWeekNo = weekNo + 1;

    let saptString = "sapt. " + otherWeekNo;

    table.find("tr:contains(" + saptString + ")").addClass("other-week-tr");

}

function insertCustomTable() {
    let table = $('<table class="table-custom">');
    table.append($('#tables-wrapper table').eq(0).find('tr:has(th)').clone());

    for (let i = 0; i < options.customTable.length; ++i) {
        let id = options.customTable[i];

        let row = $('tr[data-id="' + id + '"]');
        if (row.length > 0) {
            row = row.eq(0);
            table.append(row.clone());
        }
        else {
            console.error(id + ' NOT FOUND');
            return false;
        }
    }

    let customHeading = $('<h1>Custom</h1>');

    customHeading.prependTo("#tables-wrapper").css("margin", "30px");
    table.insertAfter(customHeading);

    return true;
}

function addDayLines(table) {
    for (let i in days) {
        let dayString = days[i];
        let last = table.find("tr:visible:contains(" + dayString + ")");

        last = $(last[last.length - 1]);
        if (!last.is(':last-child')) {
            last.addClass("last-of-day-tr");
        }
    }
}

function addDayLinesCustom(table) {
    for (let i in days) {
        let dayString = days[i];
        let last = table.find("td[title='" + dayString + "']").closest('tr');

        last = $(last[last.length - 1]);
        if (!last.is(':last-child')) {
            last.addClass("last-of-day-tr");
        }
    }
}

function getWeekNo() {
    let weekInYear = new Date().getWeek();
    let firstWeek = new Date(new Date().getYear(), 0, 1).getWeek();

    return (weekInYear - firstWeek + 1 + options.weekOffset) % 2;
}