GitHub FileSize Viewer

Show the file size next to it on the website

// ==UserScript==
// @name         GitHub FileSize Viewer
// @namespace    http://tampermonkey.net/
// @version      0.9.12
// @description  Show the file size next to it on the website
// @author       nmaxcom
// @match        https://*.github.com/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// TODO: try document.addEventListener("pjax:start", function(){...}) and pjax:end as triggers
(function(){
    /****************
     * Options:
     ****************/
    const DEBUG_MODE = true; // in production mode should be false
    const SHOW_BYTES = false; // false: always KB, i.e. '>1 KB', true: i.e. '180 B' when less than 1 KB
    /****************/

    var textColor = '#6a737d'; // Default github style
    // var textColor = '#888'; // my dark github style
    createStyles();

    var origXHROpen               = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(){
        this.addEventListener('loadend', function(){
            if(DEBUG_MODE) console.log('%cStart: ' + document.title, 'color:white;background-color:#20A6E8;padding:3px');
            tableCheckandGo();
        });
        origXHROpen.apply(this, arguments);
    };

    var vars = {}, response;
    tableCheckandGo();


    /**
     * Order of business:
     * - Detect table, if present, launch async API call and insert new blank cells
     * - Detect necessary info to make the async API call
     * - When promised is successful, change the blanks for the numbers
     * done: detect page change since github does that youtube thing
     */

    function tableCheckandGo(){
        if(document.querySelector('table.files')){
            if(setVars()){
                var callPromise = callGitHub();
                callPromise
                    .then(function(resp){
                        response = resp;
                        if(DEBUG_MODE) console.info('GitHub call went through');
                        if(DEBUG_MODE) console.info(resp.responseText);
                        insertBlankCells();
                        fillTheBlanks(JSON.parse(resp.responseText));
                        recheckAndFix();
                    })
                    .catch(function(fail){
                        if(DEBUG_MODE) console.error(fail);
                    });
            } else {
                if(DEBUG_MODE) console.info('setVars() failed. Vars: ', vars);
            }
        } else {
            if(DEBUG_MODE) console.info('No data table detected, nothing to do');
        }
    }

    /**
     * API call
     */
    function callGitHub(){
        return new Promise(function(resolve, reject){
            // I'm forced to use GM_xmlhttpRequest to avoid Same Origin Policy issues
            GM_xmlhttpRequest({
                method : "GET",
                url    : getAPIurl(),
                onload : function(res){
                    resolve(res);
                },
                onerror: function(res){
                    reject(res);
                }
            });
        });
    }

    function getAPIurl(){
        // .../repos/:owner/:repo/contents/:path?ref=branch
        vars.dir = vars.dir || '';
        return "https://api.github.com/repos/" +
            vars.owner + "/" +
            vars.repo +
            "/contents/" +
            vars.dir + "?ref=" + vars.branch;
    }

    /**
     * - Directories get new cellmate too
     *
     */
    function insertBlankCells(){
        var filenameCells = document.querySelectorAll('tr[class~="js-navigation-item"] > td.content');
        for(var i = 0, len = filenameCells.length; i < len; i++){
            var newtd       = document.createElement('td');
            newtd.className = 'filesize';
            filenameCells[i].parentNode.insertBefore(newtd, filenameCells[i].nextSibling);
        }
        if(DEBUG_MODE) console.info(`Inserted ${i} cells`);
    }

    /**
     * If we get the data, we insert it carefully so each filename gets matched
     * with the correct filesize.
     */
    function fillTheBlanks(JSONelements){
        if(!document.querySelectorAll('td.filesize').length){
            debugger;
        }
        var nametds = document.querySelectorAll('tr[class~="js-navigation-item"] > td.content a');
        var i, len;
        toploop:
            for(i = 0, len = JSONelements.length; i < len; i++){
                for(var cellnum in nametds){
                    if(nametds.hasOwnProperty(cellnum) && JSONelements[i].name === nametds[cellnum].innerHTML){
                        if(JSONelements[i].type === 'file'){
                            var sizeNumber = (JSONelements[i].size / 1024).toFixed(0);
                            if(SHOW_BYTES){
                                sizeNumber = sizeNumber < 1 ? JSONelements[i].size + ' B' : sizeNumber + ' KB';
                            } else {
                                sizeNumber = sizeNumber < 1 ? '> 1 KB' : sizeNumber + ' KB';
                            }
                            nametds[cellnum].parentNode.parentNode.nextSibling.innerHTML = sizeNumber;
                        }
                        continue toploop;
                    }
                }
            }
        if(DEBUG_MODE) console.info(`Processed ${i} of ${len} elements`);
        // if(DEBUG_MODE) console.info('Dumping json y nodes:');
        // if(DEBUG_MODE) console.log(JSONelements.forEach(function(e,i){if(DEBUG_MODE) console.log(JSONelements[i].name)}));
        // if(DEBUG_MODE) console.log(nametds.forEach(function(e,i){if(DEBUG_MODE) console.log(nametds[i].innerHTML)}));


    }

    function createStyles(){
        var css   = 'td.filesize { color: ' + textColor + ';' +
                'text-align: right;' +
                'padding-right: 50px !important; }' +
                'table.files td.message { max-width: 250px !important;',
            head  = document.head || document.getElementsByTagName('head')[0],
            style = document.createElement('style');

        style.type = 'text/css';
        if(style.styleSheet){
            style.styleSheet.cssText = css;
        } else {
            style.appendChild(document.createTextNode(css));
        }

        head.appendChild(style);
    }


    /**
     * Hay que satisfacer en la api el GET /repos/:owner/:repo/contents/:path?ref=branch
     * Con este regex capturamos del título  \w+(.*?)\sat\s(.*?)\s.*?(\w+)\/(\w+)
     * 1) dir path, 2) branch, 3) owner, 4) repo
     * Ese regex no funciona en el root
     */
    function setVars(){
        var title  = document.title;
        // Root folder:
        var match3 = title.match(/.*?([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+):/);
        // Non root folder, any branch:
        //var match1 = title.match(/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)\sat\s([a-zA-Z0-9._-]+)\s·\s([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)/);
        // Root folder, we'll extract branch from scrape
        var match2 = title.match(/.+?\/([a-zA-Z0-9._\/-]+).*?·\s([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._\/-]+)/);
        if(match3){
            vars        = {owner: match3[1], repo: match3[2]};
            vars.branch = document.querySelector('.branch-select-menu button span').innerHTML;
        }/*else if(match1) {
         vars = {repo: match1[1], dir: match1[2], branch: match1[3], owner: match1[4]};
         } */ else if(match2){
            vars        = {dir: match2[1], owner: match2[2], repo: match2[3]};
            vars.branch = document.querySelector('.branch-select-menu button span').innerHTML;
        } else if(DEBUG_MODE) console.log(getAPIurl());
        return 1;
    }

    /**
     * Sometimes, even though data has been correctly recieved, the DOM doesn't play well
     * for whatever reason. This function will quickly check if the data is indeed
     * there and if it's not will repaint the data again with the original functions.
     * TODO: finish this part
     */
    function recheckAndFix(){
        // Count td.filesize and compare to total rows
        let filesizes = document.querySelectorAll('td.filesize').length;
        let ages      = document.querySelectorAll('td.age').length;
        if(filesizes === ages){
            if(DEBUG_MODE) console.info(`Good empty check: ${filesizes} of ${ages}`);
        } else {
            if(DEBUG_MODE) console.info(`Bad empty check: ${filesizes} of ${ages}. Repainting`);

        }
        // Count non-empty td.filesize and compare to number of files from response

        if(DEBUG_MODE) console.info(`Say something...`);
    }
})();