ds3v / Gitlab review extension

// ==UserScript==
// @name         Gitlab review extension
// @version      0.7.0
// @description  File approving for gitlab!
// @author       V. Vasin
// @include      /^https?:\/\/[^/]*gitlab.[^/]+\//
// @run-at       document-body
// @license      MIT
// @copyright    2019, ds3v (https://openuserjs.org//users/ds3v)
// @icon         https://about.gitlab.com/ico/favicon-192x192.png
// @updateURL    https://openuserjs.org/meta/ds3v/Gitlab_review_extension.meta.js
// @require      https://www.gstatic.com/firebasejs/3.6.2/firebase.js
// @require      https://raw.githubusercontent.com/emn178/js-sha256/730f41e84b76d3525b5a9b505cea549ab0c2a287/build/sha256.min.js

// ==/UserScript==
(function(window) {
    'use strict';

    const STYLES =`
    .files .diff-file.approved .approve-btn {
    	background : #afa;
    	color: black;
    }
    .files.hide-approved .diff-file.approved,
    .files.hide-filtered .diff-file.filtered-file,
    .files.approve-script-applied.hide-approved .diff-file.approved,
    .files.approve-script-applied.hide-filtered .diff-file.filtered-file,
    .files.approve-script-applied .diff-file.filtered-file .diff-content,
    .files .diff-file.filtered-file .diff-content,
    .files .diff-file.filtered-file .approve-btn,
    .files .diff-file.filtered-file .bottom-approve-btn,
    .files .diff-file.approved .bottom-approve-btn,
    .files .diff-file.approved .diff-content {
    	display: none;
    }
    .files .diff-file .bottom-approve-btn {
        margin-top: -75px;
        margin-left: calc(100% - 45px);
        opacity: 0.2;
        background: #afa;
        color: #000;
        border: 1px solid #555;
        box-shadow: 0 0 3px white;
    }

    .files .diff-file .bottom-approve-btn:hover {
        opacity: 0.9
    }

    .files .diff-file .file-actions .btn.unhide-btn {
        display: none;
    }
    .files .diff-file.filtered-file .file-actions .btn.unhide-btn {
        display: inline-block;
    }
    .files .diff-file {
        position:relative;
    }
    .files.approve-script-applied .diff-file {
    	display: block;
    }
    #utilHolder #approvedCnt {
        color: green;
    }
    #utilHolder #filteredCnt {
        color: blue;
    }
    #utilHolder #filteredCnt:after,
    #utilHolder #filteredCnt:before {
        content : '/';
        color: gray;
        display: inline-block;
        width: 30px;
        text-align: center;
    }
    #utilHolder {
        position: fixed;
        top: 3px;
        left: calc(50% - 210px);
        z-index: 9999;
        background: #fff;
        border: 1px solid #eee;
        width: 420px;
        border-radius: 3px;
    }
    #utilHolder #progressCnt:before {
        content : 'Review progress:  '
    }
    #utilHolder #progressCnt {
        margin : 0 15px 0 0;
        color : red;
        font-weight: bold;
    }
    #utilHolder #progressCnt.half {
        color: #aa4500;
    }
    #utilHolder #progressCnt.done {
        color : green;
    }
    #utilHolder #setting-editor-button {
        float: left;
        margin: 5px 10px;
        font-size: 20px;
        cursor: pointer;
    }
    .hideFiltered, .hideApproved {
        margin-right:15px;
        color: black;
        cursor: pointer;
    }
    .hideApproved:before,
    .hideFiltered:before {
        margin-right: 5px;
    }
    .hideApproved.fa-eye-slash,
    .hideFiltered.fa-eye-slash {
        color: gray;
    }
    .settings-editor {
        position: fixed;
        left: calc(50% - 210px);
        top : 75px;
        z-index: 9999;
        background: #fff;
        border: 1px solid #eee;
        width: 420px;
        border-radius: 3px;
        padding : 15px;
        box-shadow: 0 0 5px black;
    }
    .settings-editor label {
       margin : 5px 0;
       display: block;
    }
    .settings-editor .repo-type {
       display: block;
    }
    .settings-editor .repo-type::after {
       margin : 5px;
       content: "Use public Firebase"
    }
    .settings-editor .buttons {
       text-align: center;
    }
    .settings-editor .buttons .btn {
       width: 100px;
       margin : 5px 10px;
    }
    .settings-editor .inner-settings {
        padding-left:20px;
    }
    `;
    let $;

    function scrollTo(element) {
        if (localStorage.noscroll === "true") {
            return;
        }
        setTimeout(() =>  {
            const offset = element.offset();
            if (offset) {
                $('html, body').animate({scrollTop: offset.top - 155}, 300);
            }
        }, 100);
    }
    class Statistic {
        constructor(parent) {
            this.progress = $("<span id='progressCnt' title='Progress'>0%</span>").appendTo(parent);
            this.approved = $("<span id='approvedCnt' title='Approved'>0</span>").appendTo(parent);
            this.filtered = $("<span id='filteredCnt' title='Filtered'>0</span>").appendTo(parent);
            this.total    = $("<span id='totalCnt' title='Total'>0</span>").appendTo(parent);
        }
        update() {
            const files = $('.files .diff-file'),
                  total = files.length || 1,
                  approved = files.filter('.approved').length,
                  filtered = files.filter('.filtered-file:not(.approved)').length,
                  processed = filtered + approved;
            this.total.text(total);
            this.approved.text(approved);
            this.filtered.text(filtered);
            this.progress
                .text((processed * 100 / total).toFixed(1) + "%")
                .toggleClass("half", processed / total >= 0.5)
                .toggleClass("done", processed === total);
        }
    }

    class Filters {
        constructor(settings) {
            this.settings = settings;
            this.preFilters = [
                this.filterByPattern
            ];
            this.fileFilters = [
                this.filterMoved,
                this.filterByContent
            ];
            this.contentFilters = [
                this.filterEmptyChanges,
                this.filterLineBreakAtTheEnd,
                this.filterByHighlightedDiff,
                this.filterImportChanges,
                this.filterPackageChanges
            ];
        }
        preFilter(changedFile) {
            const failedFilter = this.preFilters.find(f => f.call(this, changedFile));
            if (failedFilter) {
                console.debug(
                    "File '%c" + changedFile.fileName.replace(/.*\/(.*)$/, "$1") + "%c' prefiltered by %c" + failedFilter.name,
                    "color: red",
                    "color: black",
                    "color: blue; font-weight:bold"
                );
            }
            return !!failedFilter;
        }
        filter(changedFile) {
            const failedFilter = this.fileFilters.find(f => f.call(this, changedFile));
            if (failedFilter) {
                console.log(
                    "File '%c" + changedFile.fileName.replace(/.*\/(.*)$/, "$1") + "%c' filtered by %c" + failedFilter.name,
                    "color: red",
                    "color: black",
                    "color: blue; font-weight:bold"
                );
            }
            return !!failedFilter;
        }
        filterByPattern(changedFile) {
            return this.settings.filters.some(p => changedFile.fileName.indexOf(p) > -1);
        }
        filterMoved(changedFile) {
            return changedFile.element.find(".nothing-here-block:contains('File moved')").length > 0;
        }
        filterByContent(changedFile) {
            return changedFile.changedLines.every(line => this.contentFilters.some(f => f.call(this, line)));
        }
        filterByHighlightedDiff(line) {
            return (
                this.settings.filterEmptyChanges
                && line.highlightedDiff.length > 0
                && line.highlightedDiff.replace(/[\s;]+/g, "").length === 0
            );
        }
        filterEmptyChanges(line) {
            return this.settings.filterEmptyChanges && line.content.length === 0;
        }
        filterLineBreakAtTheEnd(line) {
            return this.settings.filterEmptyChanges && line.content === "}";
        }
        filterImportChanges(line) {
            return this.settings.filterImportChanges && line.content.indexOf("import ") === 0;
        }
        filterPackageChanges(line) {
            return this.settings.filterImportChanges && line.content.indexOf("package ") === 0;
        }
    }
    class SettingsEditorButton {
         constructor(parent, settings) {
             const editor = new SettingsEditor(settings);
             this.button = $("<span id='setting-editor-button' class='btn fa fa-cogs'/>")
                 .appendTo(parent)
                 .click(() => editor.show());
        }
    }
    class ReviewPanel {
        constructor(settings) {
            this.panel = $("<div id='utilHolder'/>");
            this.settingsButton = new SettingsEditorButton(this.panel, settings);
            this.statistic = new Statistic(this.panel);
            this.controls = new ReviewPanelControls(this.panel);
        }
        update() {
            this.statistic.update();
            this.controls.update();
        }
        inject() {
            this.panel.appendTo('body');
            this.update();
        }
    }
    class ReviewPanelControls {
        constructor(parent) {
            const holder = $("<div/>").appendTo(parent);
            this.hideFilteredBtn = $("<label class='hideFiltered fa fa-eye'>&nbsp;Filtered</label>")
                .appendTo(holder)
                .click(() => this.toggleFiltered());
            this.hideApprovedBtn = $("<label class='hideApproved fa fa-eye'>&nbsp;Approved</label>")
                .appendTo(holder)
                .click(() => this.toggleApproved());
        }
        toggleFiltered() {
            localStorage.hideFiltered = localStorage.hideFiltered !== "true";
            this.update();
        }

        toggleApproved() {
            localStorage.hideApproved = localStorage.hideApproved !== "true";
            this.update();
        }
        update() {
            const hideFiltered = localStorage.hideFiltered === undefined ? true : localStorage.hideFiltered === "true";
            const hideApproved = localStorage.hideApproved === "true";
            localStorage.hideFiltered = hideFiltered;
            localStorage.hideApproved = hideApproved;
            this.hideFilteredBtn.toggleClass("fa-eye-slash", hideFiltered);
            this.hideApprovedBtn.toggleClass("fa-eye-slash", hideApproved);
            $('.files')
                .toggleClass("hide-filtered", hideFiltered)
                .toggleClass("hide-approved", hideApproved);
        }
    }

    class MergeRequestInjector {
        constructor(userInfo, filters, reviewPanel, approveStateStorage, unhideStateStorage) {
            this.reviewPanel = reviewPanel;
            this.filters = filters;
            this.approveStateStorage = approveStateStorage;
            this.unhideStateStorage = unhideStateStorage;
            this.userInfo = userInfo;
        }
        inject() {
            this.injectStyles();
            this.injectActionsToFileList();
            this.reviewPanel.inject();
        }
        injectActionsToFileList() {
            if ($(".files.approve-script-applied").length) {
                return;
            }
            $('.files .diff-file').each((i, element) => this.injectActionsToFileElement($(element)));
            $('.files').addClass("approve-script-applied");
        }
        injectActionsToFileElement(fileElement) {
            const changedFile = new ChangedFile(fileElement);
            const controller = new StateController(this.approveStateStorage, changedFile);
            changedFile
                .listener(() => this.reviewPanel.update())
                .listener(() => controller.onChanges());
            this.injectActionsToChangedFile(changedFile);
            this.applyFilters(changedFile)
        }
        applyFilters(changedFile) {
            const prefiltered = this.filters.preFilter(changedFile);
            changedFile.setFiltered(prefiltered);
            if (!prefiltered) {
                changedFile.waitContent(() => changedFile.setFiltered(this.filters.filter(changedFile)));
            }
        }

        injectActionsToChangedFile(changedFile) {
            changedFile.addAction(new ApproveButton(changedFile, this.userInfo.plain(), false));
            changedFile.addAction(new ApproverIcon(changedFile));
            changedFile.addAction(new UnhideFilteredButton(this.unhideStateStorage, changedFile));
            //changedFile.addActionToDiffContent(new ApproveButton(changedFile, this.userInfo.plain(), true));
        }
        injectStyles() {
    	    $('<style>')
                .html(STYLES)
                .appendTo("head");
        }
    }

    class StateController {
        constructor(stateStorage, file) {
            this.file = file;
            this.stateStorage = stateStorage;
            this.storeId = file.fileId;
            this.init();
            this.lastApprover = undefined;
        }
        init() {
            this.stateStorage.watch(this.storeId, state => {
                const lastState = state || {};
                this.lastApprover = lastState.user;
                this.lastChecksum = lastState.checksum;
                this.file.setApprove(this.lastChecksum, this.lastApprover);
            });
        }
        onChanges() {
            const approver = this.file.getApprover();
            const checksum = this.file.getApprovedChecksum();
            if (this.lastChecksum === checksum && this.lastApprover === approver) {
                return;
            }
            if (this.file.isApproved()) {
                this.stateStorage.set(this.storeId, {
                    checksum : checksum,
                    user : approver
                });
                this.lastApprover = approver;
                this.lastChecksum = checksum;
            } else {
                this.stateStorage.set(this.storeId, {});
                this.lastApprover = undefined;
                this.lastChecksum = undefined;
            }
        }
    }

    class ApproverIcon {
        constructor(file) {
            this.element = $("<img class='avatar avatar-inline s24'>").hide();
            this.file = file;
        }
        refresh() {
            const approved = this.file.isApproved();
            console.log(this.file);
            if (approved) {
                const approver = this.file.getApprover()
                this.element
                    .attr("title", "Approved by " + approver.name)
                    .attr("src", approver.avatar)
                    .attr("srcset", approver.avatar.replace("uploads/user", "uploads/-/system/user"));
            }
            this.element.toggle(approved);
        }
    }

    class ApproveButton {
        constructor(changedFile, userInfo, bottom) {
            this.element = $("<span class='btn fa approve-btn fa-check'/>")
                .toggleClass("bottom-approve-btn", !!bottom)
                .click(() => changedFile.toggleApprover(userInfo));
        }
    }
    class UserInfo {
        constructor() {
            const link = $(".profile-link");
            const avatarUrl = $(".header-user-avatar").eq(0).attr("src");
            this.name = link.data("user");
            this.url = link.attr("href");
            this.avatar = avatarUrl && (avatarUrl.indexOf(location.host) == -1 ? avatarUrl : avatarUrl.replace(/[^:]+:\/\/[^/]+/, "")) || "";
        }
        isAuthorized() {
            return !!this.name;
        }
        plain() {
            return {
                name : this.name,
                url : this.url,
                avatar : this.avatar,
            };
        }
    }
    class UnhideFilteredButton {
        constructor(stateStorage, file) {
            this.element = $("<span class='btn fa unhide-btn fa-eye'/>")
                .click(() => this.unfilter());
            this.file = file;
            this.storeId = 'unhidden/' + this.file.fileName;
            this.stateStorage = stateStorage;
            this.init();
        }
        init() {
            this.stateStorage.watch(this.storeId, value => {
                if (value === "true") {
                    this.file.disableFilter();
                }
            });
        }
        unfilter() {
            this.file.setFiltered(false)
            this.file.scrollTo();
        }
        refresh() {
            const filtered = !!this.file.isFiltered;
            this.stateStorage.set(this.storeId, "" + filtered);
        }
    }

    class ChangedFile {
        constructor(fileElement) {
            this.fileName = $('.file-title-name', fileElement).text().replace(/[\n\r]+/gi, "").trim();
            this.fileId = sha256(this.fileName);
            this.element = fileElement;
            this._filterDisabled = false;
            this._filtered = false;
            this._approver = undefined;
            this._approvedChecksum = undefined;
            this.listeners = [];
            this.actions = [];
            this.actionsToolbar = fileElement.find(".file-actions");
            this.refresh();
        }
        listener(listener) {
            this.listeners.push(listener);
            return this;
        }
        setFiltered(state) {
            if (this._filterDisabled || state == this._filtered) {
                return
            }
            this._filtered = state;
            this.element.toggleClass("filtered-file", state);
            this.refresh();
        }

        disableFilter() {
            this.setFiltered(false);
            this._filterDisabled = true;
        }
        setApprove(approvedChecksum, approver) {
            if (approvedChecksum === this._approvedChecksum) {
                return
            }
            this._approver = approver;
            this._approvedChecksum = approvedChecksum;
            this.updateView();
        }
        toggleApprover(approver) {
            if (this.isApproved()) {
                this.setApprove(undefined, approver);
            } else {
                this.setApprove(this.checksum, approver);
            }
        }
        isApproved() {
            return !!(this._approver && this._approvedChecksum && this._approvedChecksum === this.checksum)
        }
        updateView() {
            const approved = this._approver && this._approvedChecksum && this._approvedChecksum === this.checksum;
            this.element.toggleClass("approved", !!approved);
            this.listeners.forEach(listener => listener());
            this.actions
                .filter(action => typeof action.refresh === "function")
                .forEach(action => action.refresh())
        }
        getApprover() {
            return this._approver;
        }
        getApprovedChecksum() {
            return this._approvedChecksum;
        }
        initChecksum() {
            const newLines = this.changedLines.filter(l => l.element.is(".new .line")).map(l => l.content).join("\n");
            const oldLines = this.changedLines.filter(l => l.element.is(".old .line")).map(l => l.content).join("\n");
            this.checksum =  sha256(oldLines) + " - " + sha256(newLines);
        }
        isLoaded() {
            return this.element.find(".diff-content i.fa.fa-spinner, .diff-collapsed").length === 0;
        }
        waitContent(callback) {
            if (this.isLoaded()) {
                this.refresh();
                callback();
            } else {
                setTimeout(() => this.waitContent(callback), 500);
            }
        }
        refresh() {
            this.diffContent = this.element.find(".diff-content");
            this.changedLines = [...this.diffContent.find(".new.line_content, .old.line_content")]
                .map(element => $(element).find(".line"))
                .map(line => {
                    return {
                        element: line,
                        content: line.text().trim(),
                        highlightedDiff: line.find(".idiff").text().trim()
                    };
                });
            this.initChecksum(this.element);
            this.updateView();
        }
        addAction(action) {
            this.actions.push(action);
            this.actionsToolbar.prepend(action.element);
        }
        addActionToDiffContent(action) {
            this.actions.push(action);
            this.diffContent.after(action.element);
        }
        toggleClass(className, value) {
            this.element.toggleClass(className, value);
        }
        scrollTo() {
           scrollTo(this.element);
        }
        scrollToNext() {
            let target = this.element.nextAll(":not(.filtered-file, .approved)");
            if (!target || !target.length) {
                target = this.element.prevAll(":not(.filtered-file, .approved)");
                if (!target || !target.length) {
                    target = $("body");
                }
            }
            scrollTo(target.eq(0));
        }
    }

    class Gitlab {
        constructor(settings, stateStorageFactory) {
            this.stateStorageFactory = stateStorageFactory;
            this.filters = new Filters(settings);
            this.reviewPanel = new ReviewPanel(settings);
            this.stateStorages = {};
            window.gt=this;
        }
        detectMergeRequestId(href) {
            const urlData = /\/([^/]+)\/merge_requests\/(\d+)/.exec(href);
            if (!urlData || urlData.length < 3) {
                return undefined;
            }
            return sha256(location.host) + "/" + urlData[1] + "/" + urlData[2];
        }
        waitForInjecting() {
            if (!this.mergeRequestId) {
                return;
            }
            // && $('.files .diff-file .diff-content i.fa.fa-spinner').length === 0
            const preloaderShown = $(".loading").is(":visible");
            const files = $('.files .diff-file').length;
            if (files > 0) {
                console.info("Injecting Gitlab. Found " + files + " files");
                const userInfo = new UserInfo();
                new MergeRequestInjector(
                    userInfo,
                    this.filters,
                    this.reviewPanel,
                    this.stateStorages[this.mergeRequestId],
                    new LocalStateStorage(this.mergeRequestId)
                ).inject();
            } else {
                console.debug("Waiting for injecting, files: " + files + ", preloader: " + preloaderShown);
                setTimeout(this.waitForInjecting.bind(this), 500);
            }
        }
        mergeRequestDiffDetected(mergeRequestId) {
            if (mergeRequestId) {
                this.mergeRequestId = mergeRequestId;
                this.stateStorages[mergeRequestId] = this.stateStorages[mergeRequestId] || this.stateStorageFactory.create(mergeRequestId);
                this.waitForInjecting();
            }
        }
        checkForMergeRequest() {
            if (/\/[^/]+\/[^/]+\/merge_requests\/\d+\/diffs.*/.test(location.href)) {
                let params = location.search.split(/[?&]/).filter(s => s);
                let newParams = [];
                if (params.indexOf("expand_all_diffs=1") === -1) {
                    newParams.push("expand_all_diffs=1");
                }
                if (params.indexOf("w=1") === -1) {
                    newParams.push("w=1");
                }
                if (params.indexOf("expanded=1") === -1) {
                    newParams.push("expanded=1");
                }
                if (newParams.length) {
                    newParams.unshift(...params);
                    location.search = newParams.join("&");
                } else {
                    this.mergeRequestDiffDetected(this.detectMergeRequestId(location.href));
                }
            } else {
                setTimeout(() => this.checkForMergeRequest(), 1000);
            }
        }
        init() {
            this.checkForMergeRequest();
        }
    }

    class DBStateStorage {
        constructor(database, prefix) {
            this.database = database;
            this.prefix = prefix.replace(/[\.#$\[\]]+/gi, "_");
            this.resolvers = {};
            this.repo = {};
            this.database.ref(this.prefix).on('value', snapshot => {
                const prevRepo = this.repo;
                this.repo = snapshot.val() || {};
                Object.keys(this.resolvers).forEach(key => {
                    const prevChecksum = prevRepo[key] && prevRepo[key].checksum;
                    const currChecksum = this.repo[key] && this.repo[key].checksum;
                    if (prevChecksum != currChecksum) {
                        this.resolvers[key](this.repo[key]);
                    }
                });
            });
        }
        key(subkey) {
            return this.prefix + "/" + subkey.replace(/[\.#$\[\]\\\/]+/gi, "_");
        }
        watch(subkey, resolve) {
            this.resolvers[subkey] = resolve;
            if (this.repo[subkey]) {
                resolve(this.repo[subkey]);
            }
        }
        set(key, value) {
            setTimeout(() => {
                this.database.ref().update({
                   [this.key(key)] : value
                });
            }, 1);
        }
    }

    class DBStateStorageFactory {
        constructor(settings) {
            DBStateStorageFactory.init(settings);
        }
        create(prefix) {
            return new DBStateStorage(firebase.database(), prefix);
        }
        static init(settings) {
            if (!DBStateStorageFactory.initiated) {
                DBStateStorageFactory.initiated = true;
                localStorage.removeItem("firebase:previous_websocket_failure");
                firebase.initializeApp({
                   apiKey: settings.fireBaseApiKey,
                   databaseURL: settings.fireBaseDBUrl
                });
            }
        }
    }

    class LocalStateStorage {
        constructor(prefix) {
            this.prefix = prefix;
        }
        watch(key, resolve) {
            var value = localStorage[this.prefix + "/" + key];
            resolve(value === undefined ? value : JSON.parse(value));
        }
        set(key, value) {
            localStorage[this.prefix + "/" + key] = JSON.stringify(value);
        }
    }
    class LocalStateStorageFactory {
        create(prefix) {
            return new LocalStateStorage(prefix);
        }
    }

    class Main {
        static init() {
            const settings = new Settings();
            new Gitlab(settings, this.createStateStorage(settings)).init();
        }
        static createStateStorage(settings) {
            if (settings.stateStorage === "local") {
                console.info("Local state storage");
                return new LocalStateStorageFactory();
            }
            console.info("DB state storage");
            return new DBStateStorageFactory(settings);
        }
    }

    class Settings {
        constructor() {
            function get(name) {
                return localStorage[name] || Settings.defaults[name];
            }
            function getBool(name, trueValue="true") {
                return get(name) === trueValue;
            }
            this.stateStorage = get("stateStorage");
            this.isPublicFireBase = (
                getBool("fireBaseDBUrl", Settings.defaults.fireBaseDBUrl) && getBool("fireBaseApiKey", Settings.defaults.fireBaseApiKey)
            );
            this.fireBaseApiKey = get("fireBaseApiKey");
            this.fireBaseDBUrl = get("fireBaseDBUrl");
            this.filterPackageChanges = getBool("filterPackageChanges");
            this.filterImportChanges = getBool("filterImportChanges");
            this.filterEmptyChanges = getBool("filterEmptyChanges");
            this.filters = JSON.parse(get("fileNameFilters")).filter(a => a && a.length > 2);
            this.enableFileNameFilter = this.filters.length !== 0 && getBool("enableFileNameFilter");
        }
        update() {
            localStorage.stateStorage = this.stateStorage;
            localStorage.fireBaseApiKey = this.fireBaseApiKey;
            localStorage.fireBaseDBUrl = this.fireBaseDBUrl;
            localStorage.filterPackageChanges = this.filterPackageChanges;
            localStorage.filterImportChanges = this.filterImportChanges;
            localStorage.filterEmptyChanges = this.filterEmptyChanges;
            localStorage.enableFileNameFilter = this.enableFileNameFilter;
            localStorage.fileNameFilters = JSON.stringify(this.filters);
        }
    }
    Settings.defaults = {
        stateStorage : "db",
        filterPackageChanges : "true",
        filterImportChanges : "true",
        filterEmptyChanges : "true",
        enableFileNameFilter : "false",
        fileNameFilters : "[]",
        fireBaseApiKey : "AIzaSyDh6mkW61kb8HbmRZ6gZQjeuZugWTDpLto",
        fireBaseDBUrl : "https://public-gitlab-review.firebaseio.com"
    };

    class SettingsEditor {
        constructor(settings) {
            function checkbox(holder, toggleable) {
                const checkbox = $("<input type='checkbox'>")
                    .appendTo(holder)
                    .wrap("<div class='checkbox'></div>")
                    .wrap("<label></label>");
                return toggleable ? checkbox.change(() => toggleable.toggle(checkbox.is(":checked"))) : checkbox;
            }
            const editor = $("<div class='settings-editor'>").hide();
            const repoHolder = $("<div class='repo-holder'/>").appendTo(editor);
            const filtersHolder = $("<div>").appendTo(editor);
            const buttons = $("<div class='buttons'>").appendTo(editor);
            const saveBtn = $("<button class='btn btn-primary'>Save</button>")
                .appendTo(buttons)
                .click(() => this.save());
            const cancelBtn = $("<button class='btn btn-default'>Cancel</button>")
                .appendTo(buttons)
                .click(() => this.close());
            this.settings = settings;
            this.editor = editor;
            this.firebaseSettings = $("<div/>").addClass("inner-settings");
            this.useDbCheckbox = checkbox(repoHolder, this.firebaseSettings).after("Use firebase DB for sharing approves");
            this.firebaseSettings.appendTo(repoHolder);
            this.urlInput = $("<input class='form-control'>")
                .appendTo(this.firebaseSettings)
                .wrap("<label>Firebase DB url: </label>");
            this.apiKeyInput = $("<input class='form-control'>")
                .appendTo(this.firebaseSettings)
                .wrap("<label>Firebase Api key: </label>");
            this.filterImportChanges = checkbox(filtersHolder).after("Filter for 'import' changes");
            this.filterEmptyChanges = checkbox(filtersHolder).after("Filter for 'empty' changes");
            this.fileNameFilterHolder = $("<div/>").addClass("inner-settings");
            this.enableFileNameFilter = checkbox(filtersHolder, this.fileNameFilterHolder).after("Filter by file names");
            this.fileNameFilterHolder.appendTo(filtersHolder);
            this.filtersInput = $('<textarea rows=7 class="form-control"></textarea>')
                .appendTo(this.fileNameFilterHolder)
                .wrap("<label>File name filters(exclude): </label>");
        }
        show() {
            this.update();
            this.editor.show().appendTo("body");
        }
        update() {
            this.urlInput.val(this.settings.fireBaseDBUrl);
            this.apiKeyInput.val(this.settings.fireBaseApiKey);
            this.filtersInput.val(this.settings.filters.join("\n"));
            const isDatabaseStateStorage = this.settings.stateStorage === "db";
            this.useDbCheckbox.prop("checked", isDatabaseStateStorage);
            this.firebaseSettings.toggle(isDatabaseStateStorage);
            this.filterEmptyChanges.prop("checked", this.settings.filterEmptyChanges);
            this.filterImportChanges.prop("checked", this.settings.filterImportChanges);
            this.enableFileNameFilter.prop("checked", this.settings.enableFileNameFilter);
            this.fileNameFilterHolder.toggle(this.settings.enableFileNameFilter);
        }
        save() {
            this.settings.fireBaseDBUrl = this.urlInput.val();
            this.settings.fireBaseApiKey = this.apiKeyInput.val();
            this.settings.filters = this.filtersInput.val()
                .split("\n")
                .map(s => s.trim())
                .filter(s => s.length > 0)
                .filter((v, i, arr) => arr.indexOf(v) === i);
            this.settings.stateStorage = this.useDbCheckbox.is(":checked") ? "db" : "local";
            this.settings.filterImportChanges = this.filterImportChanges.is(":checked");
            this.settings.filterPackageChanges = this.filterImportChanges.is(":checked");
            this.settings.filterEmptyChanges = this.filterEmptyChanges.is(":checked");
            this.settings.enableFileNameFilter = this.enableFileNameFilter.is(":checked");
            this.settings.update();
            this.close();
        }
        close() {
            this.editor.hide();
        }
    }

   function init() {
       if (!window.$) {
           setTimeout(init, 200);
       } else {
           $ = window.$;
           Main.init();
       }
   }
   init();

})(window.document.defaultView);