Greasy Fork

NicoLiveCleaner

ニコ生各種ページにおいて特定生主を非表示にする。NGワード(正規表現)でも可能。

当前为 2019-04-26 提交的版本,查看 最新版本

// ==UserScript==
// @name         NicoLiveCleaner
// @namespace    https://greasyfork.org/ja/users/292779-kinako
// @version      1.04
// @description  ニコ生各種ページにおいて特定生主を非表示にする。NGワード(正規表現)でも可能。
// @author       kinako
// @include      http*://live.nicovideo.jp/*
// @include      http*://live2.nicovideo.jp/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-body
// @compatible   chrome
// ==/UserScript==

(function() {
    'use strict';

    /*
      汚 ( ヾハハヘ
      物 (  |W/ヘヘ
      は (  |‖//ヘヘ
      消 (  ハ‖イ///彡
      毒 ( /丶/ ̄\ミ
      だ ( /ヘ / ̄‖ミ
      | ( レ=  ̄ =、 ‖ミミ
      | ( Y三八三>=ヘミミ
      !! (〈 L_ソ 〉ノミミ、
      ⌒⌒ Y 戸弌 i|\ミミ
       __i kェェノ / //
      /  /`ー―イ //
      ヒ_(王)二二二二(王)ノ
      L_|_‖  /  | ‖ ̄
      彡彡\\ |  |o‖
      彡彡彡L||  |o‖
      彡彡//ー仝ー-ヽ/ ̄
      |イ二‾\____/ /
      |i二 |   | |.
      丶て_ム___| |.
  */

    class Controller
    {
        constructor(model)
        {
            this._model = model;
        }

        main(targets)
        {
            targets = (Object.prototype.toString.call(targets) === '[object Array]')? targets: [targets];
            this._model.setBodyMObserver(targets);
        }

        receive(flag, data = null)
        {
            switch(flag) {
                case 'record':
                    this._model.setData('ng_numbers', data);
                    this._model.iniNGData();
                    break;
                case 'delete':
                    this._model.deleteData('ng_numbers', data);
                    break;
                case 'ngreg':
                    this._model.setRegData('ng_keywords', data);
                    location.reload();
                    break;
                case 'InitializeData':
                    this._model.iniNGData();
                    break;
            }
        }
    }


    class Model
    {
        constructor()
        {
            this.ng_comment_keywords = null;
            this.targets = null;
            this._ng = {pattern:null, c_pattern:null, numbers:null, keywords:null, c_keywords:null};
            this.iniNGData();
        }

        set ng(val){this._ng.pattern = val};
        get ng(){return this._ng.pattern};
        set ng_numbers(val){this._ng.numbers = val};
        get ng_numbers(){return this._ng.numbers};
        set ng_keywords(val){this._ng.keywords = val};
        get ng_keywords(){return this._ng.keywords};

        //ページロード
        setBodyMObserver(targets, options={childList: true})
        {
            this.targets = targets;
            const mo = new MutationObserver(this.setMObserver.bind(this));
            mo.observe(document.body, options);
            window.addEventListener("DOMContentLoaded", function(e){
                mo.disconnect();
            });
        }

        // 動的部分監視
        setMObserver(mr, mo)
        {
            for(let i = 0; i < this.targets.length; i++)
            {
                if (this.targets[i].Loaded())
                {
                    this.dispatchEvent(this.targets[i]);
                    if (this.targets[i].mo_options)
                    {
                        let mo = new MutationObserver( (mr, _mo)=>{
                            _mo.disconnect();
                            this.dispatchEvent(this.targets[i]);
                            _mo.observe(this.targets[i].element, this.targets[i].mo_options);
                        });
                        mo.observe(this.targets[i].element, this.targets[i].mo_options);
                    }
                }
            }
        }

        dispatchEvent(target)
        {
            let el = (target.element.children)? target.element.children: target.element;

            let event = new Event(target.eventType);
            for (let x = el.length; x > 0; x--){
                el[x-1].addEventListener(target.eventType, target.callback);
                el[x-1].dispatchEvent(event);
            }
        }

        // NGデータ設定
        iniNGData()
        {
            // NGコミニュティ番号
            let ls = this.getLocalStorage('ng_numbers');
            if(ls) {
                ls = ls.filter(function(str) {
                    if (/^(co|ch)[0-9]+$/.test(str)) { // コミュニティ&チャンネル
                        return str + "\\."
                    } else if (/^[0-9]+$/.test(str)) { // ユーザー番号
                        return '/' + str + "\\.";
                    }
                });
                this.ng_numbers = ls;
                ls = (ls)? ls.join('|'): '';
            } else {
                ls = '';
                this.ng_numbers = [];
            }

            // NGキーワード
            let ls2 = this.getLocalStorage('ng_keywords');
            if (ls2) {
                ls2 = ls2.filter(function(str) {
                    if (str !== "" || str !== undefined) return str;
                });
                this.ng_keywords = ls2;
                ls2 = (ls2)? ls2.join('|'): '';
            } else {
                ls2 = '';
                this.ng_keywords = [];
            }
            // 結合
            let sep = (ls != '' && ls2 != '')? '|': '';
            this.ng = ls + sep + ls2;
        }

        setData(key, data)
        {
            let ls = this.getLocalStorage(key);
            if (ls) {
                if (!ls.includes(data))
                {
                    ls.push(data);
                    this.setLocalStorage(key, ls);
                }
            } else {
                this.setLocalStorage(key, [data]);
            }
        }

        setRegData(key, data)
        {
            data = data.trim().split('\n');
            data = data.filter(function(str) {
                if (str !== "" || str !== undefined) return str;
            });
            this.setLocalStorage(key, data);
        }

        deleteData(key, data)
        {
            let ls = this.getLocalStorage(key);
            if (ls)
            {
                let newArray = ls.filter(n => n !== data);
                this.setLocalStorage(key, newArray);
            }

        }

        clearData(key){ GM_deleteValue(key); }

        getLocalStorage(key)
        {
            let ls = GM_getValue(key);
            if (ls)
            {
                //return (!Array.isArray(ls))? JSON.parse(ls): ls;
                return this.checkArray(ls);
            }
            return null;
        }
        checkArray(data)
        {
            if (!Array.isArray(data))
            {
                return this.checkArray(JSON.parse(data));
            } else {
                return data;
            }
        }

        setLocalStorage(key,data){
            GM_setValue(key, JSON.stringify(data));
        }
    }


    class View
    {
        constructor(controller)
        {
            this.controller = controller;
            this.targets = [];
            this.comment_ng = null;
            this.video_ng = null;
            this.css_prefix = 'nlc';
            this._ng = {pattern:null, c_pattern:null, numbers:null, keywords:null, c_keywords:null};

            this.setStyle();
        }

        get ng(){return this._ng.pattern};
        get ng_numbers(){return this._ng.numbers};
        get ng_keywords(){return this._ng.keywords};

        // スタイルシート設定
        setStyle()
        {
            let css = document.createElement('style')
            let rule = document.createTextNode(`
/* 削除ボタン */
button.${this.css_prefix}-btn {
  font-size: 12px;
  color: #fff;
  padding: 0 3px;
  border-radius: 4px;
  opacity: 0.05;
  background-color: #404040;
  border: none;
}
/* 削除ボタンホバー時 */
button.${this.css_prefix}-btn:hover {
  border: none;
  opacity: 1;
}

/* ライブページ 削除ボタン */
button[id^="${this.css_prefix}-live-"] {
  opacity: 1;
}
/* ライブページ 削除ボタンホバー時 */
button[id^="${this.css_prefix}-live-"]:hover {
  border: none;
  opacity: 0.8;
}
/* ライブページ 状態コンテナ */
span[id^="${this.css_prefix}-btncontainer"],
span[id^="${this.css_prefix}-state"] {
  margin-left: 1em;
}
/* ライブページ NG中状態 */
span[id^="${this.css_prefix}-state"], span[id^="${this.css_prefix}-userstate"] {
  letter-spacing: 0.2em;
  width: 4em;
  text-align: center;
  color: #FF0066;
  background-color: #FFF;
  border: 1px solid #FF0066;
  padding: 0em 0.5em;
  border-radius: 4px;
}
/* ライブページ ユーザー状態コンテナ */
 span[id^="${this.css_prefix}-usercontainer"], span[id^="${this.css_prefix}-userstate"] {
  margin-left:0.3em;
}
/* ライブページ ユーザーNG中状態 */
span[id^="${this.css_prefix}-userstate"] {
    padding: 0 0.2em;
    letter-spacing: 0;
    font-size: 11px;
}
/* ライブページザッピング 削除ボタン*/
button[id^="${this.css_prefix}-user"] {
  opacity: 0.1;
  z-index: 2;
}

/* ライブページザッピング 削除ボタンコンテナ */
div.${this.css_prefix}-zapp-btn-container {
  margin: -20px 0 0 0;
  width: 100%;
  text-align: right;
  position: absolute;
  z-index: 2;
}
/* ライブページザッピング 削除ボタン*/
button[id^="${this.css_prefix}-zapp"] {
  opacity: 0.1;
  z-index: 2;
}

/* 検索ページ 削除ボタン */
button[id^="${this.css_prefix}-search"] {
  height: 1.5em;
  opacity: 0.1;
}
/* トップページ 削除ボタン */
button[id^="${this.css_prefix}-top"] {
  opacity: 0.5;
}
/* 番組一覧 削除ボタン */
button[id^="${this.css_prefix}-proglist"] {
  padding: 0 3px 0 2px;
}

/* ボタンコンテナ */
div.${this.css_prefix}-proglist-btn-container,
div.${this.css_prefix}-ps4list-btn-container,
div.${this.css_prefix}-rankpic-btn-container,
div.${this.css_prefix}-channel-btn-container,
div.${this.css_prefix}-top-btn-container,
div.${this.css_prefix}-search-btn-container,
div.${this.css_prefix}-search2-btn-container,
div.${this.css_prefix}-search3-btn-container,
span.${this.css_prefix}-modal-img-containner,
div.${this.css_prefix}-left-btn-container {
  margin: -1.5em 0 0 0;
  width : 100%;
  text-align: right;
  position: relative;
}
div.${this.css_prefix}-top-btn-container {
  margin: -2px 0 0 0px;
  position: absolute;
}
div.${this.css_prefix}-search3-btn-container {
  margin: 0 0 0 -1em;
  position: absolute;
}
span.${this.css_prefix}-modal-img-containner {
  margin:0em 0 0 0;
}
div.${this.css_prefix}-right-btn-container {
    width: 100%;
    text-align: right;
    margin-top: 4.5em;
    position: absolute;
}


/* オーバーレイ */
div#${this.css_prefix}-overlay {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.7);
  opacity: 0;
  transition: opacity 0.3s;
  z-index: 10000;
}

/* モーダル */
div#${this.css_prefix}-modal-contents {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 500px;
  height: auto;
  text-align: left;
  padding: 2em;
  transform: translate(-50%, -50%);
  background: #fff;
  overflow-x: hidden;
 /* overflow-y: scroll;*/
  border-radius: 10px;
}

#${this.css_prefix}-modal-contents > div:nth-child(2) {
  margin-bottom: 1em;
}

div#${this.css_prefix}-modal-contents a {text-decoration: none;}
div#${this.css_prefix}-modal-contents a:link,
div#${this.css_prefix}-modal-contents a:visited{
  color:#808080;
}
div#${this.css_prefix}-modal-contents a:hover{
  color:#A6A6A6;
}

div#${this.css_prefix}-modal-contents h2 {
  margin: 0 0 3px 0;
  font-size: 13px;
}
div#${this.css_prefix}-modal-contents img {
  width: 40px;
  height: 40px;
  border-radius: 5px;
}
div#${this.css_prefix}-modal-contents textarea {
  width: 100%;
  height: 10em;
  padding : 0.5em 0 0.5em 0.3em;
}

/* モーダルNGコミュニティ削除ボタン */
button[id^="${this.css_prefix}-ngimg"] {
  margin: 0 0 0 -18px;
}
/* モーダルupdateボタン */
button#${this.css_prefix}-regb {
  padding: 0 1em;
  background-color: #0080FF;
  opacity: 1;
  margin: 1em 0 0 0;
}
button#${this.css_prefix}-regb:hover { opacity: 0.5;}

/* モーダルcloseボタン */
#${this.css_prefix}-close {
  text-align: center;
  width: 100%;
  margin-top: 2em;
  font-size: 15px;
}

/* NGコミュナビゲーション */
#${this.css_prefix}-navi { margin: 0.5em 0 2.5em 0; }
#${this.css_prefix}-navi a,
#${this.css_prefix}-navi-on{
  font-size: 12px;
  display: inline-block;
  width: 2em;
  border: 1px solid #0080FF;
  color: #0080FF;
  text-align: center;
  border-radius: 6px;
  background-color: #FFF;
  margin-right: 0.2em;
}
#${this.css_prefix}-navi-on {
  background-color:#0080FF !important;
  color: #FFF !important;
}
`);
            css.media = 'screen';
            css.type = 'text/css';
            if (css.styleSheet) {
                css.styleSheet.cssText = rule.nodeValue;
            } else {
                css.appendChild(rule);
            };
            document.getElementsByTagName('head')[0].appendChild(css);
        }

        // ページ分岐
        main()
        {
            this.insertMenu();
            this.liveTopPage();
            this.liveFocusPage();
            this.liveListPage();
            this.liveSearchPage();
            this.liveProgramPage();
            this.liveProgramPage2();
            this.liveRankingPage();
            this.liveTimetablePage();
            this.controller.main(this.targets);
        }

        // 削除
        clean(target, ng = this.ng)
        {
            if (this.checkNG(target, ng)) {
                target.remove();
            }
        }

        // HTMLCollection削除(監視無し)
        cleanAll(target, ng = this.ng)
        {
            const t = new Target();
            t.element = (toString.call(target) === '[object Function]')? target: ()=> target;
            t.eventType = 'cleanAllLoaded';
            t.callback = (event)=>{this.clean(event.target, ng);};
            this.targets.push(t);
        }

        // HTMLCollection用ループ削除
        loopClean(targets, ng = this.ng)
        {
            for (let i = targets.length; i > 0; i--) {
                this.clean(targets[i-1], ng);
            }
        }

        // 削除だと表示が乱れる場合は非表示
        hide(target, ng = this.ng)
        {
            if (this.checkNG(target, ng)) {
                const style = target.getAttribute('style');
                const r = new RegExp('display:none');
                if (!r.test(style)) {
                    target.setAttribute('style', 'display:none;' + style);
                }
            }
        }

        // HTMLCollection用 非表示
        loopHide(targets, ng = this.ng)
        {
            for (let i = targets.length; i > 0; i--) {
                this.hide(targets[i-1], ng);
            }
        }

        checkNG(target, ng)
        {
            if (ng != null && ng.length > 0 && target) {
                const r = new RegExp(ng);
                return (r.test(target.outerHTML))? true: false;
            }
        }

        parseCoNumber(t, id = null)
        {
            if(t != null)
            {
                let m = /[\/>]{1}((co|ch)[0-9]+)/mg.exec(t.outerHTML);
                if (!m) {
                    m = /\/user\/([0-9]+)/mg.exec(t.outerHTML);
                    if (m) {
                        m[2] = 'user';
                    }
                }

                if (m && id && document.getElementById(this.css_prefix+'-'+id+'-'+m[1]))
                {
                    for (let i = 0; i < t.children.length; i++)
                    {
                        if (new RegExp('^'+this.css_prefix+'-').test(t.children[i].getAttribute('class'))
                            || new RegExp('^'+this.css_prefix+'-').test(t.children[i].id) )
                        {
                            t.children[i].remove();
                        }
                    }
                }
                return (m)? {number:m[1], prefix:m[2]}: null;
            }
            return null;
        }

        convertID(id, number)
        {
            return this.css_prefix +'-' + id + '-' + number;
        }


        // 生放送ページタイトル部分に削除ボタン表示
        displayDeleteButtonLiveRoom(t, id)
        {
            if (t == null) return;
            let pcn = this.parseCoNumber(t, id);
            if(pcn)
            {
                let _id = this.convertID(id, pcn.number);

                let pb = document.createElement('span');
                pb.id = this.convertID('btncontainer', pcn.number);

                let text = document.createElement('span');
                text.id = this.convertID('state', pcn.number);
                text.textContent = 'NG中';

                // 追加済み
                if (new RegExp(this.ng).test(pcn.number+'.'))
                {
                    if (!document.getElementById(pb.id)) {
                        pb = text;
                    }

                // 追加する
                } else {
                    let b = this.iniButton(_id, 'x');
                    b.addEventListener('click', (e)=>{
                        pb.remove();
                        pb = text;
                        t.appendChild(pb);
                        this.controller.receive('record', pcn.number);
                     });
                     pb.appendChild(b);
                }
                t.appendChild(pb);
            }

        }

        displayDeleteButtonLiveUser(t, id)
        {
            if (t == null) return;

            let pcn = this.parseCoNumber(t.parentNode, id);
            if(pcn)
            {
                let _id = this.convertID(id, pcn.number);

                let pb = document.createElement('span');
                pb.id = this.convertID('usercontainer', pcn.number);

                let text = document.createElement('span');
                text.id = this.convertID('userstate', pcn.number);
                text.textContent = 'NG中';

                // 追加済み
                if (new RegExp(this.ng).test('/'+pcn.number+'\.'))
                {
                    if (!document.getElementById(pb.id)) {
                        pb = text;
                    }

                    // 追加する
                } else {
                    let b = this.iniButton(_id, 'x');
                    b.addEventListener('click', (e)=>{
                        pb.remove();
                        pb = text;
                        t.appendChild(pb);
                        this.controller.receive('record', pcn.number);
                    });
                    pb.appendChild(b);
                }
                t.appendChild(pb);
            }

        }

        displayDeleteButtonSingle(t, id)
        {
            if (t == null) return;
            let pcn = this.parseCoNumber(t, id);
            if(pcn)
            {
                let _id = this.convertID(id, pcn.number);

                let pb = document.createElement('div');
                pb.setAttribute('class', this.css_prefix +'-'+id+ '-btn-container');
                let b = this.iniButton(_id, 'x');

                pb.appendChild(b);
                t.appendChild(pb);

                b.addEventListener('click', (e)=>{
                    e.stopPropagation();
                    t.remove();
                    this.controller.receive('record', pcn.number);
                });
            }
        }
        displayDeleteButton(targets, id)
        {
            for (let i = 0; i < targets.length; i++) {
                this.displayDeleteButtonSingle( targets[i], id);
            }
        }



        insertMenu()
        {
            if (!location.href.match(/http(s)*:\/\/live(2*)\.nicovideo\.jp\//)) return;

            window.addEventListener("DOMContentLoaded", function(e){
                // メニュー部分取得
                const menu = document.getElementById('siteHeaderRightMenuContainer');

                const li = document.createElement('li');
                const a = document.createElement('a');
                a.id = 'NicoLiveCleaner';
                a.textContent = 'NicoLiveCleaner';
                li.appendChild(a);

                // ログインアウトリンクの一つ手前に挿入
                menu.lastElementChild.parentNode.insertBefore(li, menu.lastElementChild);

                a.addEventListener('click', (e)=>{
                    //メニュー非表示に
                    menu.style.marginRight = '-100em';
                    this.displayModal(a.id);
                });
            }.bind(this));

        }

        displayModal(id)
        {
            let overlay, modal, close;

            // オーバーレイ
            overlay= document.createElement('div');
            overlay.setAttribute('id', this.css_prefix + '-overlay');

            // モーダルボックス
            modal= document.createElement('div');
            modal.setAttribute('id', this.css_prefix + '-modal-contents');

            // クローズリンク
            let cp = document.createElement('div');
            cp.id = this.css_prefix + '-close';
            close = document.createElement('a');
            close.setAttribute('id', 'close');
            close.setAttribute('href', '#');
            close.innerHTML = 'CLOSE';

            // 見出し
            let ng_num_title = document.createElement('h2');
            ng_num_title.textContent = 'NGコミュニティ&チャンネル&ユーザー';

            let ng_reg_title = document.createElement('h2');
            ng_reg_title.textContent = 'NGキーワード(正規表現)';

            modal.appendChild(ng_num_title);
            let ng_container = document.createElement('div');
            this.ng_numbers.reverse();
            ng_container.appendChild(this.iniNgNumber(this.ng_numbers, 50, 0));
            ng_container.appendChild(this.iniNavi(this.ng_numbers, 50, 0));
            modal.appendChild(ng_container);

            modal.appendChild(ng_reg_title);
            modal.appendChild(this.iniNgRegEx(this.ng_keywords));
            cp.appendChild(close);
            modal.appendChild(cp);

            overlay.appendChild(modal);

            // コンテンツを表示
            document.body.appendChild(overlay);
            overlay.style.display = 'block';
            overlay.style.opacity = '1';

            // モーダル親要素への伝播防止
            modal.addEventListener('click', function(e) {
                e.stopPropagation();
            });

            // オーバーレイ削除
            overlay.addEventListener('click', function() {
                this.style.opacity = '0';
                setTimeout(function() {
                    this.remove();
                    location.reload();
                }.bind(this), 300);
            });

            // クローズ経由でオーバーレイ削除
            close.addEventListener('click', function(e) {
                e.preventDefault();
                overlay.style.opacity = '0';
                setTimeout(function() {
                    overlay.remove();
                    location.reload();
                }, 300);
            });
        }

        iniNavi(items, block, position)
        {
            let page = Math.ceil(items.length / block);
            let navi = document.createElement('div');
            navi.id = this.css_prefix + '-navi'
            let navi_on = this.css_prefix + '-navi-on';

            if (page > position){
                for (let i = 0; i < page; i++) {
                    let span = document.createElement('span');
                    let a = document.createElement('a');
                    if (i == position) {
                        a.id = navi_on;
                    }
                    a.addEventListener('click', function(e) {
                        console.log(navi);
                        for (let x = 0; x < navi.children.length; x++)
                        {
                            if (navi.children[x].id == navi_on) {
                                navi.children[x].removeAttribute('id');
                            } else if (x == i) {
                                navi.children[x].id = navi_on;
                            }
                        }
                        let parent_id = this.css_prefix + '-ngnumbers';

                        let base = document.getElementById(parent_id).parentNode;
                        document.getElementById(parent_id).remove();
                        this.controller.receive('InitializeData');
                        this.ng_numbers.reverse();
                        base.insertBefore(this.iniNgNumber(this.ng_numbers, block, i), base.firstChild);
                    }.bind(this));

                    span.textContent = i + 1;
                    a.appendChild(span);
                    navi.appendChild(a);
                }
            }
            return navi;
        }

        iniNgNumber(items, block, position = 0)
        {
            let outer = document.createElement('div');
            outer.id = this.css_prefix + '-ngnumbers';

            const start = block * position;
            const end = start + block;
            const block_items = items.slice(start, end);

            if (block_items)
            {
                for (let i = 0; i < block_items.length; i++)
                {
                    // URL生成
                    let f = new RegExp('(co|ch)[0-9]+').exec(block_items[i]);
                    let img_pref, a_url;
                    let img_url = 'https://secure-dcdn.cdn.nimg.jp/comch/';
                    if(f != null)
                    {
                        if (f[1] == 'co') {
                            img_pref = 'community';
                            a_url =['com', img_pref];

                        } else if (f[1] == 'ch') {
                            img_pref = 'channel';
                            a_url = ['ch', img_pref];
                        }
                        img_url += img_pref + '-icon/64x64/'+block_items[i]+'.jpg';

                    } else if (/^[0-9]+/.test(block_items[i])){
                        img_url = 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/s/'
                        img_url += block_items[i].substr(0, block_items[i].length-4)+'/'+block_items[i]+'.jpg';
                        a_url = ['www', 'user'];
                    }

                    let img = document.createElement('img');
                    img.onerror = function() {
                        img.src = 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank_s.jpg'; //代用
                    }
                    img.src = img_url;
                    img.alt = block_items[i];
                    img.title = img.alt;

                    let a = document.createElement('a');
                    a.href = 'https://'+ a_url[0] +'.nicovideo.jp/'+ a_url[1] +'/'+ block_items[i];
                    a.target = '_blank';

                    let pb = document.createElement('span');
                    pb.setAttribute('class', this.css_prefix + '-modal-img-container');

                    let b = this.iniButton(this.convertID('ngimg', block_items[i]), 'x');

                    a.appendChild(img);
                    pb.appendChild(a);
                    pb.appendChild(b);
                    outer.appendChild(pb);

                    b.addEventListener('click', (e)=>{
                        e.stopPropagation();
                        pb.remove();
                        this.controller.receive('delete', block_items[i]);
                    });
                }

            }
            return outer;

        }


        iniNgRegEx(items)
        {
            let outer = document.createElement('div');
            let btn = this.iniButton(this.css_prefix+'-regb', 'UPDATE');

            let ta = document.createElement('textarea');

            if (Array.isArray(items))
            {
                ta.value = items.join('\n');
            }
            outer.appendChild(ta);
            outer.appendChild(btn);

            btn.addEventListener('mousedown', (e)=> {
                btn.style.opacity = '0.2';
                this.controller.receive('ngreg', ta.value);
            });
            return outer;
        }



        iniButton(id, value)
        {
            let btn = document.createElement('button');
            btn.setAttribute('id', id);
            btn.setAttribute('class', this.css_prefix + '-btn');
            btn.setAttribute('type', 'button');
            btn.textContent = value;
            return btn;
        }



        /*
        *    ニコ生トップページ
        */
        liveTopPage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp(\/\?header|\/)$/)) return;

            const programs = new Target();
            programs.element = ()=> document.getElementsByClassName('___contents-area___1DChO')[0];
            programs.eventType = 'focus-program-listMoLoaded';
            programs.mo_options = {childList: true, subtree:true};
            programs.callback = (event)=>{
                this.loopHide(event.target.getElementsByClassName('___item___1FRJr'));
                this.displayDeleteButton(event.target.getElementsByClassName(
                    '___program-card___3oc72 ___program-card___3iSz5'), 'top');
            };
            this.targets.push(programs);
        }


        /*
        *     ニコ生注目番組一覧ページ
        */
        liveFocusPage()
        {
            if (!location.href.match(/http(s)*:\/\/live2\.nicovideo\.jp\/focus$/)) return;

            const programs = new Target();
            programs.element = ()=> document.getElementsByClassName('___contents-area___1wpA_')[0];
            programs.eventType = 'contentslistMoLoaded';
            programs.mo_options = {childList: true, subtree:true};
            programs.callback = (event)=>{this.loopHide(event.target.getElementsByClassName('___item___1FRJr'));};
            this.targets.push(programs);

            const programs2 = new Target();
            programs2.element = ()=> document.getElementsByClassName('___feature-program-section___2bZFd')[0];
            programs2.eventType = 'featurelistMoLoaded';
            programs2.mo_options = {childList: true, subtree:true};
            programs2.callback = (event)=>{this.loopHide(event.target.getElementsByClassName('___item___1FRJr'));};
            this.targets.push(programs2);
        }


        /*
        *     ニコ生番組一覧ページ
        */
        liveListPage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp\/recent\?.*/)) return;

            // 番組一覧監視
            const programs = new Target();
            programs.element = ()=> document.getElementById('onair_stream_list');
            programs.eventType = 'user-program-listMoLoaded';
            programs.mo_options = {childList: true};
            programs.callback = (event)=>{
                this.loopClean(event.target.getElementsByClassName('user-program-item'));
                this.displayDeleteButton(event.target.getElementsByClassName('user-program-item'), 'proglist');
            };
            this.targets.push(programs);

            // ランキング&ピックアップ監視
            const ranking = new Target();
            ranking.element = ()=> document.getElementById('contents-block');
            ranking.eventType = 'rankinglistMoLoaded';
            ranking.mo_options = {childList: true, subtree:true};
            ranking.callback = (event)=>{
                this.loopClean(event.target.getElementsByTagName('li'));
                this.displayDeleteButton(event.target.getElementsByTagName('li'), 'rankpic');
            };
            this.targets.push(ranking);

            // チャンネル部分削除(ページ下部)
            const channel = new Target();
            channel.element = ()=> document.getElementById('channel-block');
            channel.eventType = 'channelMoLoaded';
            channel.callback = (event)=>{
                this.loopClean(event.target.getElementsByTagName('li'));
                this.displayDeleteButton(event.target.getElementsByTagName('li'), 'channel');
            };
            this.targets.push(channel);

            // PS4配信ピックアップ欄
            const ps4 = new Target();
            ps4.element = ()=> document.getElementById('eden_pickup_program_area');
            ps4.eventType = 'ps4-program-listMoLoaded';
            ps4.callback = (event)=>{
                this.loopClean(event.target.getElementsByClassName('ps4-program-item'));
                this.displayDeleteButton(event.target.getElementsByClassName('ps4-program-item'), 'ps4list');
            };
            this.targets.push(ps4);
        }


        /*
        *     ニコ生検索ページ
        */
        liveSearchPage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp\/search\?.*/)) return;

            // 検索結果
            const t = new Target();
            t.element = ()=> document.getElementsByClassName('result-item');
            t.eventType = 'cleanAllLoaded';
            t.callback = (event)=>{
                this.clean(event.target, this.ng);
                this.displayDeleteButtonSingle(event.target, 'search');
            };
            this.targets.push(t);


            // 関連チャンネル
            const channel = new Target();
            channel.element = ()=> document.getElementsByClassName('recommend-channel-list')[0];
            channel.eventType = 'recommend-channelMoLoaded';
            channel.mo_options = {childList: true, subtree:true, attributes: true};
            channel.callback = (event)=>{
                this.hide(event.target);
                this.displayDeleteButtonSingle(event.target, 'search2');
            };
            this.targets.push(channel);

            // 関連コミュニティ
            const community = new Target();
            community.element = ()=> document.getElementsByClassName('recommend-community-list')[0];
            community.eventType = 'recommend-communityMoLoaded';
            community.mo_options = {childList: true, subtree:true, attributes: true};
            community.callback = (event)=>{
                this.hide(event.target);
                this.displayDeleteButtonSingle(event.target, 'search3');
            };
            this.targets.push(community);
        }

        /*
        *    ニコ生旧ライブページ&放送終了後ページ
        */
        liveProgramPage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp\/(watch|gate)\/lv.*/)) return;

            // ザッピング番組監視
            const zapping = new Target();
            zapping.element = ()=> document.getElementById('zapping_area');
            zapping.eventType = 'zappingMoLoaded';
            zapping.mo_options = {childList: true, subtree:true};
            zapping.callback = (event)=>{this.loopHide(event.target.getElementsByClassName('zapping_stream'));};
            this.targets.push(zapping);

            // こんな番組も見ています
            const after = new Target();
            after.element = ()=> document.getElementsByClassName('gyokuon_container')[0];
            after.eventType = 'gyokuonMoLoaded';
            after.mo_options = {childList: true};
            after.callback = (event)=>{ this.loopHide(event.target.getElementsByClassName('gyokuon_list_item')); };
            this.targets.push(after);
        }


        /*
        *    ニコ生html5ライブページ
        */
        liveProgramPage2()
        {
            if (!location.href.match(/http(s)*:\/\/live2\.nicovideo\.jp\/watch\/lv.*/)) return;

            // コミュニティ付近にNGボタン表示
            const info = new Target();
            info.element = ()=> document.getElementsByClassName('___header-area___1blqo')[0];
            info.eventType = 'infoMoLoaded';
            //info.mo_options = {childList: true, subtree:true};
            info.callback = (event)=>{
                this.displayDeleteButtonLiveRoom(document.getElementsByClassName('___header-area___1blqo')[0], 'live');
            }
            this.targets.push(info);

            // ユーザー付近にNGボタン表示
            const user = new Target();
            user.element = ()=> document.getElementsByClassName('___user-summary___3OBKx')[0];
            user.eventType = 'userLoaded';
            user.callback = (event)=>{
                this.displayDeleteButtonLiveUser(event.target.getElementsByClassName('___user-name-area___d4F8v')[0], 'user');

            };
            this.targets.push(user);

            // ザッピング番組監視
            const zapping = new Target();
            zapping.element = ()=> document.getElementsByClassName('___zapping-list-group___cD_qH')[0];
            zapping.eventType = 'zappingMoLoaded';
            zapping.mo_options = {childList: true, subtree:true};
            zapping.callback = (event)=>{
                this.loopClean( event.target.getElementsByClassName('___zapping-item___38v3z ___zapping-item___3kO6M') );
                this.displayDeleteButton( event.target.getElementsByClassName('___zapping-item___38v3z ___zapping-item___3kO6M'), 'zapp');};
            this.targets.push(zapping);
        }



        /*
        *    ニコ生ランキングページ
        */
        liveRankingPage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp\/ranking.*/)) return;

            const ranking = new Target();
            ranking.element = ()=> document.getElementById('wrapper');
            ranking.eventType = 'rankingMoLoaded';
            ranking.mo_options = {childList: true, subtree:true, attributes: true};
            ranking.callback = (event)=>{
                this.loopClean(event.target.getElementsByClassName('user clearfix ranking_video'));
                this.loopClean(event.target.getElementsByTagName('li'));
                this.displayDeleteButton( event.target.getElementsByClassName('user clearfix ranking_video'), 'left');
                this.displayDeleteButton( event.target.getElementsByTagName('li'), 'right');
            };
            this.targets.push(ranking);
        }


        /*
        *    ニコ生番組表ページ
        */
        liveTimetablePage()
        {
            if (!location.href.match(/http(s)*:\/\/live\.nicovideo\.jp\/timetable.*/)) return;

            const list = new Target();
            list.element = ()=> document.getElementsByTagName('tbody')[0];
            list.eventType = 'TimetableMoLoaded';
            list.mo_options = {childList: true, subtree:true};
            list.callback = (event)=>{this.clean(event.target);};
            this.targets.push(list);
        }
    }



    class Target
    {
        constructor()
        {
            this._element = null; // 対象部分
            this.eventType = null; // イベント名
            this.callback = null; // イベントコールバック
            this.mo_options = null; // MutationObserverオプション {childList: true, subtree:true, attributes: true};
        }

        set element(val) { this._element = val; }
        get element() { return (typeof this._element == 'function')? this._element() : this._element; }

        Loaded(name = 'loaded', val='')
        {
            if (Object.prototype.toString.call(this.element) === "[object HTMLCollection]") {
                if (this.element.length > 0) {
                    let last = this.element[this.element.length - 1];
                    return this.checkload(last);
                } else {
                    return false;
                }
            } else {
                return this.checkload(this.element);
            }
        }

        checkload(element, name = 'loaded', val='')
        {
            if(element && element.hasAttribute(name) == false) {
                element.setAttribute(name, val);
                return true;
            } else {
                return false;
            }
        }
    }


    const model = new Model();
    const con = new Controller(model);
    const view = new View(con);
    view._ng = model._ng;
    view.main();

})();