User:Mahir256/subtitle-editor.js

Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
// Subtitles parser from https://github.com/bazh/subtitles-parser
// Originally by bazh <interesno@gmail.com> (X11 License: https://github.com/bazh/subtitles-parser/blob/master/LICENSE)

var parser = (function() {
    var pItems = {};

    pItems.timeMs = function(val) {
        var regex = /(\d+):(\d{2}):(\d{2}),(\d{3})/;
        var parts = regex.exec(val);

        if (parts === null) {
            return 0;
        }

        for (var i = 1; i < 5; i++) {
            parts[i] = parseInt(parts[i], 10);
            if (isNaN(parts[i])) parts[i] = 0;
        }

        // hours + minutes + seconds + ms
        return parts[1] * 3600000 + parts[2] * 60000 + parts[3] * 1000 + parts[4];
    };

    pItems.msTime = function(val) {
        var measures = [ 3600000, 60000, 1000 ]; 
        var time = [];

        for (var i in measures) {
            var res = (val / measures[i] >> 0).toString();
            
            if (res.length < 2) res = '0' + res;
            val %= measures[i];
            time.push(res);
        }

        var ms = val.toString();
        if (ms.length < 3) {
            for (i = 0; i <= 3 - ms.length; i++) ms = '0' + ms;
        }

        return time.join(':') + ',' + ms;
    };

    /**
     * Converts SubRip subtitles into array of objects
     * [{
     *     id:        `Number of subtitle`
     *     startTime: `Start time of subtitle`
     *     endTime:   `End time of subtitle
     *     text: `Text of subtitle`
     * }]
     *
     * @param  {String}  data SubRip suntitles string
     * @param  {Boolean} ms   Optional: use milliseconds for startTime and endTime
     * @return {Array}  
     */
    pItems.fromSrt = function(data, ms) {
        var useMs = ms ? true : false;

        data = data.replace(/\r/g, '');
        var regex = /(\d+)\n(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})/g;
        data = data.split(regex);
        data.shift();

        var items = [];
        for (var i = 0; i < data.length; i += 4) {
            items.push({
                id: data[i].trim(),
                startTime: useMs ? pItems.timeMs(data[i + 1].trim()) : data[i + 1].trim(),
                endTime: useMs ? pItems.timeMs(data[i + 2].trim()) : data[i + 2].trim(),
                text: data[i + 3].trim()
            });
        }

        return items;
    };

    /**
     * Converts Array of objects created by this module to SubRip subtitles
     * @param  {Array}  data
     * @return {String}      SubRip subtitles string
     */
    pItems.toSrt = function(data) {
        if (!data instanceof Array) return '';
        var res = '';

        for (var i = 0; i < data.length; i++) {
            var s = data[i];

            if (!isNaN(s.startTime) && !isNaN(s.endTime)) {
                s.startTime = pItems.msTime(parseInt(s.startTime, 10));
                s.endTime = pItems.msTime(parseInt(s.endTime, 10));
            }

            res += s.id + '\n';
            res += s.startTime + ' --> ' + s.endTime + '\n';
            res += s.text.replace('\r\n', '\n') + '\n\n';
        }

        return res;
    };

    return pItems;
})();

// All the code below is adapted from [[d:MediaWiki:Gadget-labelLister.js/beta.js]]

importStylesheet("User:Mahir256/subtitle-editor.css");

var subtitleEditor = ( function (mw, $){
    "use strict";

    /**
     * Get the ID of the current entity. (if it's not an entity it terminate the script)
     */
    var pageName = mw.config.get( 'wgPageName' );
    if ( !pageName ) {
        return;
    }

    /**
     * Get the user's language
     */
    var lang = mw.config.get( 'wgUserLanguage' );

    /**
     * Write a line on the table
     */
    function LlTable (elem){
        this.tableElement = elem;
        this.diff = {};
        this.subs = [];
        this.buttons = $("<div class='ll-buttons'><div class='ll-button ll-button-moveup'>🢁</div><div class='ll-button ll-button-movedown'>🢃</div><div class='ll-button ll-button-insertbelow'>🢆</div><div class='ll-button ll-button-insertabove'>🢅</div><div class='ll-button ll-button-delete'>🞮</div></div>");
    }

    LlTable.prototype = {
        addCol: function(lang, type, data){
            //Condition
            if(typeof lang != "string"){
                throw "You've sent the wrong type of data through lang param of LlTable.addCol()";
            }
            if(typeof type != "string"){
                throw "You've sent the wrong type of data through type param of LlTable.addCol()";
            }

            var ret = '<div class="ll-col ll-col-' + type + '" lang="' + lang + '">';
            if(typeof data == "object" && data instanceof Array){
                ret += '<ul class="ll-lists ll-' + type + '" lang="' + lang + '">';
                for(var key in data){
                    if(!data.hasOwnProperty(key)){
                        continue;
                    }
                    ret += '<li class="ll-list" lang="' + lang + '">' + mw.html.escape( data[key] ) + '</li>';
                }
                ret += '<li class="ll-list ll-list-add" lang="' + lang + '">+</li>';
                ret += '</ul>';
            }
            if(typeof data == "string"){
                ret += '<div class="ll-single ll-' + type + '"  lang="' + lang + '">' + mw.html.escape( data ) + '</div>';
            }
            ret += "</div>";
            return ret;
        },
        addRow: function(data){
            if(typeof data != "object"){
                throw "You've sent the wrong type of data through data param of LlTable.addRow()";
            }

            //First line
            var ret = '<div class="ll-row" data-index="' + data.id + '" lang="' + lang + '">';
            ret += this.addCol(lang, "index", data.id);
            ret += this.addCol(lang, "startTime", data.startTime);
            ret += this.addCol(lang, "endTime", data.endTime);
            ret += this.addCol(lang, "text", data.text);
            ret += '</div>';

            return ret;
        },
        addRows: function(data){
            if(typeof data != "object"){
                throw "You've sent the wrong type of data through data param of LlTable.addRows()";
            }
            this.subs = data;
            //Reset (no opened language, empty table, close edition)
            this.unEditCols();
            $(this.tableElement).empty();
            var ret = '';

            //fetch each line
            for(var key in data){
                try{
                    ret = this.addRow(data[key]);
                    $(this.tableElement).append(ret);
                }catch (e){
                    console.log(e);
                }
            }

            this.setEvent();

        },
        editCol: function(e){
            if(!$(e).hasClass("ll-index") && !$(e).hasClass("ll-pending-edition")){
                table.unEditCols();
                $(e).removeClass("ll-deleted");
                $(e).removeClass("ll-edited");
                $(e).addClass("ll-pending-edition");

                var inner = e.textContent;
                if(typeof $(e).prop("old") == "undefined"){
                    $(e).prop('old', inner);
                }
                var old = $(e).prop("old");

                if($(e).hasClass("ll-text")){
                    $(e).html('<textarea id="lli" rows="2" placeholder="' + old + '">' + mw.html.escape( inner ) + '</textarea>');
                }
                else {
                    $(e).html('<input id="lli" placeholder="' + old + '" value="' + mw.html.escape( inner ) + '"/>');
                }

                $('#lli').focus();
            }
        },
        unEditCols: function(){
            $(".ll-pending-edition").each(function () {
                $(this).removeClass("ll-pending-edition");

                //Easter egg (whoops!!)
                var rE = new RegExp("whoops!!");
                // handle inputs
                if($(this).children('input').length != 0){
                    if(rE.test($(this).children('input').val())){
                        $(this).children('input').val($(this).prop("old"));
                    }
                    var inner = $(this).children('input').val();
                    var old = $(this).prop("old");
                    if(!$(this).hasClass("ll-list")) {
                        $(this).removeClass("ll-deleted");
                        $(this).removeClass("ll-edited");
                        if (inner === "" && old !== "") {
                            $(this).addClass("ll-deleted");
                        } else if (inner != old) {
                            $(this).addClass("ll-edited");
                        }
                    }else{
                        $(this).addClass("ll-list-added");
                    }
                    $(this).text(inner);
                }
                // handle textareas
                if($(this).children('textarea').length != 0){
                    if(rE.test($(this).children('textarea').val())){
                        $(this).children('textarea').val($(this).prop("old"));
                    }
                    var inner = $(this).children('textarea').val();
                    var old = $(this).prop("old");
                    if(!$(this).hasClass("ll-list")) {
                        $(this).removeClass("ll-deleted");
                        $(this).removeClass("ll-edited");
                        if (inner === "" && old !== "") {
                            $(this).addClass("ll-deleted");
                        } else if (inner != old) {
                            $(this).addClass("ll-edited");
                        }
                    }else{
                        $(this).addClass("ll-list-added");
                    }
                    $(this).text(inner);
                }
                //prevent empty list
                if(inner === "" && $(this).hasClass("ll-list")){
                    table.deleteList(this);
                }
            });
        },
        addList: function(e){
            table.unEditCols();
            var appendice = '<li class="ll-list ll-pending-edition" old="" lang="' + $(e).prop("lang") + '"><input/></li>';
            $(e).before(appendice);
        },
        deleteList: function(e){
            table.unEditCols();
            $(e).remove();
        },
        removeList: function(e){
            table.unEditCols();
            if(!$(e).hasClass("ll-list-removed")) {
                $(e).addClass("ll-list-removed");
            }else{
                $(e).removeClass("ll-list-removed");
            }
        },
        swapRows: function(earlierrow, laterrow){
            var findex = $(earlierrow).find(".ll-index").text();
            var fstart = parser.timeMs($(earlierrow).find(".ll-startTime").text());
            var fend = parser.timeMs($(earlierrow).find(".ll-endTime").text());

            var lindex = $(laterrow).find(".ll-index").text();
            var lstart = parser.timeMs($(laterrow).find(".ll-startTime").text());
            var lend = parser.timeMs($(laterrow).find(".ll-endTime").text());

            // adjust the row contents
            // todo: make sure original subtitle position is preserved across moves
            // todo: if new = old for some part of a row, unset "ll-edited" for that part
            $(laterrow).find(".ll-index").prop("old",lindex).text(findex).addClass("ll-edited");
            $(laterrow).find(".ll-startTime").text(parser.msTime(fstart));
            $(laterrow).find(".ll-endTime").text(parser.msTime(fstart + (lend-lstart)));
            $(earlierrow).find(".ll-index").prop("old",findex).text(lindex).addClass("ll-edited");
            $(earlierrow).find(".ll-startTime").text(parser.msTime(lend - (fend-fstart)));
            $(earlierrow).find(".ll-endTime").text(parser.msTime(lend));

            // perform the swap
            $(laterrow).after($(earlierrow));
            $(laterrow).attr("data-index",findex);
            $(earlierrow).attr("data-index",lindex);
        },
        setEvent: function () {
            //add event on data
            $(".ll-row").off("click");
            $(".ll-col-startTime,.ll-col-endTime,.ll-col-text").each(function () {
                var cell = $(this).children('.ll-single');
                if(!cell.hasClass("ll-event")) {
                    cell.addClass("ll-event");
                    $(this).click(function () {
                        if(!$(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.editCol(cell.get(0));

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });
            $(".ll-row").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(".ll-row").mouseover(function(){
                        table.buttons.prependTo($(this).find(".ll-col-index"))
                    }).mouseleave(function(){
                        $(this).find(".ll-buttons").remove();
                    });
                }
            });
            //add event on add list button
            $(".ll-list-add").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        if(!$(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.addList(this);

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });

            //add event on add list button
            $(".ll-list-added").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        if(!$(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.deleteList(this);

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });

            //add event on add list button
            $(".ll-list").each(function () {
                if(!$(this).hasClass("ll-event") && !$(this).hasClass("ll-list-added") && !$(this).hasClass("ll-list-add") && !$(this).hasClass("ll-pending-edition")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        if(!$(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.removeList(this);

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });
            $(".ll-index").click(function(){
                var startTime = $(this).parents(".ll-row").children('.ll-col-startTime').text();
                var endTime = $(this).parents(".ll-row").children('.ll-col-endTime').text();
                var pausing_function = function(){
                    if(this.currentTime < startTime || this.currentTime >= endTime){
                        this.pause();
                        this.removeEventListener("timeupdate",pausing_function);
                        $(".ll-col-index-playing").removeClass("ll-col-index-playing");
                    }
                }
                startTime = startTime.replace(',','.').split(':').reduce((acc,time) => (60 * acc) + +time);
                endTime = endTime.replace(',','.').split(':').reduce((acc,time) => (60 * acc) + +time);
                var editorVideo = $(".ll-videopane video").get(0);
                editorVideo.currentTime = startTime;
                editorVideo.addEventListener('timeupdate',pausing_function);
                $(this).addClass("ll-col-index-playing");
                editorVideo.play();
            });
            //close edition on return
            $(".ll-pending-edition input, .ll-pending-edition textarea")
                .blur(function () {
                    table.unEditCols();
                    table.setEvent();
                });
            $(".ll-pending-edition input")
                .blur(function () {
                    table.unEditCols();
                    table.setEvent();
                })
                .keyup(function (e) {
                    if(e.which == 13 || e.which == 10) {
                        table.unEditCols();
                        table.setEvent();
                    }
                });
            $(".ll-pending-edition textarea")
                .keyup(function (e) {
                    if(e.ctrlKey && (e.which == 13 || e.which == 10)) {
                        table.unEditCols();
                        table.setEvent();
                    }
                });

            //Accept changes
            $(".ll-preview-send").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        if($(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            var resume = $(".ll-preview-resume").val();

                            if(model.sendData(table.diff, resume)){
                                table.addRows(model.receiveData());
                                table.unlock();
                            }

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });

            //Cancel changes
            $(".ll-preview-return").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        console.log(table.tableElement);
                        if($(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.addRows(model.receiveData());
                            table.setDiff(table.diff);
                            table.unlock();
                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });

            //Cancel changes
            $(".ll-preview-cancel").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        if($(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {

                            table.addRows(model.receiveData());
                            table.unlock();

                            //update all event
                            table.setEvent();
                        }
                    });
                }
            });

            //Display information
            $(".ll-info").each(function () {
                if(!$(this).hasClass("ll-event")) {
                    $(this).addClass("ll-event");
                    $(this).click(function () {
                        alert("\n== Label & Description ==\n" +
                            " * To edit label and description just click on it.\n" +
                            " * Blue and red ones are those which are changed from the original.\n" +
                            " * Type \"whoops!!\" without quote anywhere in a field to return to the original value.\n" +
                            "\n== Aliases ==\n" +
                            " * To delete an alias from the original click on it, it becomes red. To undo a deletion re-click on it.\n" +
                            " * To create an alias click on the (+), it creates a new blue one. To cancel a creation from the original just re-click, it will disappear.\n" +
                            " * To edit an alias. Delete it from the original then create a new one.\n" +
                            " * Blue ones are those which will be created and red ones are those which woill be deleted from the original.\n" +
                            "\n== Language ==\n" +
                            " * If a language doesn't appear in the table, click on (add new language) to add a new one.\n" +
                            " * You can search for a language in the field (search a language).\n" +
                            "\n== Other ==\n" +
                            " * Click on (reload) the re-download data from the server\n" +
                            " * Click on preview to see your change. You need to preview before saving.\n" +
                            "\n== Preview ==\n" +
                            " * Set a summary to explain your change. A summary of what you've changed is automatically added after your summary.");

                        //update all event
                        table.setEvent();
                    });
                }
            });
            $(".ll-row").on("click",".ll-button-moveup",function(){
                var currow = $(this).parents(".ll-row");
                var prevrow = currow.prev();
                if(!$.isEmptyObject(prevrow)){
                    table.swapRows(prevrow, currow);
                }
            });
            $(".ll-row").on("click",".ll-button-movedown",function(){
                var currow = $(this).parents(".ll-row");
                var nextrow = currow.next();
                if(!$.isEmptyObject(nextrow)){
                    table.swapRows(currow, nextrow);
                }
            });
            $(".ll-row").on("click",".ll-button-insertbelow",function(){
                // get current row details
                var currow = $(this).parents(".ll-row");
                var fend = parser.timeMs($(currow).find(".ll-endTime").text());
                // get next row details
                var lindex = parseInt(currow.attr("data-index"))+1;
                var lstart = fend + 5000;
                var nextrow = currow.next();
                if(!$.isEmptyObject(nextrow)){
                    lstart = parser.timeMs($(nextrow).find(".ll-startTime").text());
                }
                // create new row
                var newrow = $(table.addRow({
                    id: lindex.toString(),
                    startTime: parser.msTime(fend+50),
                    endTime: parser.msTime(lstart-50),
                    text: ""
                }));
                $(currow).after($(newrow));
                // adjust rest of rows
                $(newrow).nextAll().each(function(){
                    var newindex = (parseInt($(this).attr("data-index"))+1).toString();
                    $(this).attr("data-index",newindex);
                    $(this).find(".ll-index").text(newindex);
                })
                table.setEvent();
            });
            $(".ll-row").on("click",".ll-button-insertabove",function(){
                // get current row details
                var currow = $(this).parents(".ll-row");
                var lstart = parser.timeMs($(currow).find(".ll-startTime").text());
                // get next row details
                var findex = parseInt(currow.attr("data-index"));
                var fend = lstart - 5000;
                var prevrow = currow.prev();
                if(!$.isEmptyObject(prevrow)){
                    fend = parser.timeMs($(prevrow).find(".ll-endTime").text());
                }
                // create new row
                var newrow = $(table.addRow({
                    id: findex.toString(),
                    startTime: parser.msTime(fend+50),
                    endTime: parser.msTime(lstart-50),
                    text: ""
                }));
                $(currow).before($(newrow));
                // adjust rest of rows
                $(newrow).nextAll().each(function(){
                    var newindex = (parseInt($(this).attr("data-index"))+1).toString();
                    $(this).attr("data-index",newindex);
                    $(this).find(".ll-index").text(newindex);
                })
                table.setEvent();
            });
            $(".ll-row").on("click",".ll-button-delete",function(){
                // get current row details
                var currow = $(this).parents(".ll-row");
                var nextrow = currow.next();
                $(currow).remove();
                // adjust rest of rows
                if(!$.isEmptyObject(nextrow)){
                    var newindex = (parseInt($(nextrow).attr("data-index"))-1).toString();
                    $(nextrow).attr("data-index",newindex);
                    $(nextrow).find(".ll-index").text(newindex);
                    $(nextrow).nextAll().each(function(){
                        var newindex = (parseInt($(this).attr("data-index"))-1).toString();
                        $(this).attr("data-index",newindex);
                        $(this).find(".ll-index").text(newindex);
                    })
                }
                table.setEvent();
            });
        },
        getDiff: function () {
            var ret = {};
            $(".ll-row").each( function(){
                var row = $(this);
                var index = row.attr("data-index");
                if(!ret.hasOwnProperty(index)){
                    ret[index] = {modified: 0};
                    ["index", "startTime", "endTime", "text"].forEach(function(key){
                        row.find(".ll-col-"+key+" .ll-edited").each(function(){
                            ret[index][key] = {type: 1};
                            ret[index][key].new = $(this).text();
                            ret[index][key].old = $(this).prop("old");
                            ret[index].modified = 1;
                        });
                        row.find(".ll-col-"+key+" .ll-deleted").each(function(){
                            ret[index][key] = {type: -1};
                            ret[index][key].new = "[REMOVED]";
                            ret[index][key].old = $(this).prop("old");
                            ret[index].modified = 1;
                        });
                        row.find(".ll-col-"+key+" :not(.ll-deleted):not(.ll-edited)").each(function(){
                            ret[index][key] = {type: 0};
                            ret[index][key].new = $(this).text();
                        });
                    });
                }
            });
            table.diff = ret;
            return ret;
        },
        setDiff: function (data) {
            for(var index in data){
                var row = $(".ll-row[data-index="+index+"]");
                ["index", "startTime", "endTime", "text"].forEach(function(key){
                    if(data[index][key].type != 0){
                        var oldprop = data[index][key].old;
                        var replacementtext = "";
                        var newclass = 'll-deleted';
                        if(data[index][key].type == 1){
                            replacementtext = data[index][key].new;
                            newclass = 'll-edited';
                        }
                        row.find(".ll-"+key).prop("old",oldprop)
                                            .text(replacementtext)
                                            .addClass(newclass);
                    }
                });
            }
        },
        showDiffs: function () {
            var data = this.getDiff();
            this.tableElement.empty();

            var ret = "";

            for(var index in data){
                if(data[index].modified != 0){
                    ret += "<div class='ll-row'>";
                    ["index", "startTime", "endTime", "text"].forEach(function(key){
                        ret += '<div class="ll-preview-col ll-preview-' + key + '">';
                        if(data[index][key].type == 0){
                            ret += data[index][key].new;
                        }
                        else if(data[index][key].type == 1){
                            ret += '<div class="ll-preview-old">' + data[index][key].old + '</div>';
                            ret += '<div class="ll-preview-new">' + data[index][key].new + '</div>';
                        }
                        else if(data[index][key].type == -1){
                            ret += '<div class="ll-preview-del">' + data[index][key].old + '</div>';
                        }
                        ret += '</div>';
                    });
                    ret += '</div>';
                }
            }

            this.tableElement.html(ret);
            this.initResumeInput();

            return ret;
        },
        lock: function () {
            this.unEditCols();
            $(table.tableElement).parents(".ll-table").addClass("ll-table-blocked");
        },
        unlock: function () {
            this.unEditCols();
            $(table.tableElement).parents(".ll-table").removeClass("ll-table-blocked");
        },
        head: function ( number ) {
            var allHead = [1, 2];

            if(allHead.indexOf(number) == -1){
                throw "Wrong head number";
            }

            for(var i in allHead){
                if(!allHead.hasOwnProperty(i)){
                    continue;
                }

                $(table.tableElement).parents(".ll-table").removeClass("ll-head" + allHead[i] );
                $(".ll-form").removeClass("ll-form" + allHead[i] );
            }

            $(".ll-form").addClass("ll-form" + number );

            $(table.tableElement).parents(".ll-table").addClass("ll-head" + number );
        },
        initResumeInput: function () {
            $(".ll-preview-resume").prop("placeholder", "modified subtitles") // todo: reinsert some sort of summary
        }
    };

    /**
     * Write a line on the table
     */
    function LlModel (Qid){
        this.item = Qid;
        this.lastrevid = 0;
    }

    LlModel.prototype = {
        api: function (req, data) {
            return $.ajax({
                type: req,
                url: mw.util.wikiScript('api'),
                async: false,
                data: data
            });
        },
        view2subrip: function (data) {
            var modified_subs = [];
            for(var index in data){
                modified_subs.push({ // todo: better handle deleted rows
                    id: data[index]["index"].new,
                    startTime: data[index]["startTime"].new,
                    endTime: data[index]["endTime"].new,
                    text: data[index]["text"].new
                });
            }
            return parser.toSrt(modified_subs);
        },
        subrip2view: function (data) {
            return parser.fromSrt(data["query"]["pages"][0]["revisions"][0]["content"]);
        },
        sendData: function ( data, resume ) {
            var newtext = this.view2subrip(data);

            if(this.lastrevid == 0){
                console.log("Error 418", "I’m a teapot!", false, true);
                return false;
            }

            var success = false;

            this.api("POST", {
                action: "edit",
                title: this.item,
                summary: "modified subtitles", // todo: reinsert some sort of summary
                text: newtext,
                token: mw.user.tokens.get( 'csrfToken' ),
                baserevid: this.lastrevid,
                format: 'json'
            }).done(function (data) {
                if(data.hasOwnProperty("error")){
                    console.log("Error", "(" + data.error.code + ") " + data.error.info, true, false);
                }else if(data.hasOwnProperty("edit")){
                    if(data["edit"].hasOwnProperty("result")){
                        if(data.edit.result == "Success"){
                            console.log("Success", "This edit has been correctly sent to the database", false, true);
                            success = true;
                        }
                    }
                }
            }).fail(function () {
                alert("The request to the API failed.");
            });

            return success;

        },
        receiveData: function () {
            var ret = {};

            table.head(1);
            this.api("GET", {
                action: "query",
                prop: "revisions",
                titles: pageName,
                rvprop: "content|ids",
                formatversion: "2",
                format: "json"
            }).done(function (data) {
                if(data.hasOwnProperty("error")){
                    console.log("Error", "(" + data.error.code + ") " + data.error.info, true, false);
                    return {};
                }
                try {
                	model.lastrevid = data["query"]["pages"][0]["revisions"][0]["revid"];
                } catch (e) {
                	return;
                }
                ret = model.subrip2view(data);
            }).fail(function () {

                alert("The request to the API failed.");

            });


            return ret;
        }
    };

    /**
     * Object size
     */
    function objectSize( obj ) {
        var size = 0;
        for(var key in obj){
            if(!obj.hasOwnProperty(key)){
                continue;
            }
            size++
        }
        return size;
    };

    /**
     * Init instances of Class
     */
    var table, model;

    /**
     * Init portlet and modal on the view
     */
    function initCurrentPage () {
        //Modal Content
        var modalContent =
            '<div id="subtitleEditorv0">' +
            '<form class="ll-form" onsubmit="return false;">' +
            '<div class="ll-twocol">' +
            '<div class="ll-tablecontainer">' +
            '<div class="ll-table wikitable">' +
            '<div class="ll-tablehead">' +
            '<div class="ll-head ll-head-lang">Index</div>' +
            '<div class="ll-head ll-head-data">Start time</div>' +
            '<div class="ll-head ll-head-type">End time</div>' +
            '<div class="ll-head ll-head-lang">Text</div>' +
            '</div>' +
            '<div class="ll-tablecells">' +
            '</div>' +
            '</div>' +
            '</div>' +
            '<div class="ll-videopane">' +
            '</div>' +
            '</div>' +
            '<div class="ll-preview-accept">' +
            '<div><label for="ll-summary">Summary :</label></div>' +
            '<input type="text" id="ll-summary" class="ll-preview-resume"/>' +
            '<button class="ll-preview-send">Accept changes</button>' +
            '<button class="ll-preview-return">Return to edits</button>' +
            '<button class="ll-preview-cancel">Cancel</button>' +
            '</div>' +
            '</form>' +
            '</div>';

        //Modal location
        var modalExec;
        if ( document.getElementById( 'content' ) ) {
            modalExec = $( modalContent ).appendTo( '#content' );
        } else {
            modalExec = $( modalContent ).appendTo( '#mw_content' );
        }

        //Modal Function
        mw.loader.using( ['jquery.ui'], function () {
            modalExec.dialog( {
                title: '<img id="subtitleEditor-logo" src="//upload.wikimedia.org/wikipedia/commons/d/db/Symbol_list_class.svg" style="width:20px"> Subtitle editor',
                autoOpen: false,
                modal: true,
                width: 1100,
                height: 650,
                show: 'blind',
                hide: 'blind',
                buttons: [ {
                    text: "Preview",
                    id: 'll-preview',
                    click: function () {
                        if(!$(table.tableElement).parents(".ll-table").hasClass("ll-table-blocked")) {
                            table.unEditCols();
                            table.lock();
                            table.head(2);
                            table.showDiffs();
                        }
                    }
                },{
                    text: "Reload",
                    id: 'll-reload',
                    click: function () {
                        table.lock();
                        table.addRows(model.receiveData());
                        table.unlock();
                    }
                }, {
                    text: "Close",
                    click: function () {
                        $( this ).dialog( 'close' );
                    }
                } ]
            } );
        });

        //Portlet Link options
        if(mw.config.get('wgNamespaceNumber') == 102) {
            var portletLinkNextNode = $.uls.data.isRtl( lang ) ? '#ca-view' : '#ca-history';
            var portletId = ( mw.config.get( 'skin' ) === 'vector' ) ? 'p-views' : 'p-cactions';
            var portletLink = mw.util.addPortletLink( portletId, '#', "Subtitle editor (BETA)", 't-subtitleEditor', "Edit subtitles", 'e', portletLinkNextNode );
        }

        //Portlet Link Action
        $( portletLink ).click( function () {
            $( 'div#subtitleEditorv0' ).dialog( 'open' );
            $( 'div#subtitleEditorv0' ).dialog({
                beforeClose: function(event, ui){
                    $(".ll-videopane").empty()
                }
            });
            $('.ll-videopane').html($("#mwe_player_0").attr("videopayload"));
            $('.ll-videopane video').get(0).pause();
            $('.ll-videopane .mediaContainer').css('width',400);
            $('.ll-videopane video').css('width',400)
                                    .css('height','');
            //On opening
            table.addRows(model.receiveData());
            table.unlock();
            return false;
        } );

        //Init model Class
        model = new LlModel (pageName);

        //Init table Class
        table = new LlTable ($(".ll-table .ll-tablecells"));
    }
    
    /**
     * LOADER COMPONENT
     */
    function preventLoader(){
        var semaphore = false;
        var interval = setInterval(function(){
            if(!semaphore){
                semaphore = true;
                initCurrentPage();
                window.clearInterval(interval);
            }
        },100);
    }

    $( preventLoader );

    var ret = new Object();
    ret.table = table;
    ret.model = model;
    return new LlTable();

}  ( mediaWiki, jQuery ) );