YouTube Live - Word mute

YouTubeのチャット欄にワードミュート機能を追加します

// ==UserScript==
// @name         YouTube Live - Word mute
// @namespace    https://twitter.com/4chouyou
// @version      0.1.2
// @description  YouTubeのチャット欄にワードミュート機能を追加します
// @author       mufuuuu
// @match        https://www.youtube.com/live_chat*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==
/* jshint esversion: 6 */

(() => {
    const TIMEOUT = 10000;
    const STRINGS = {
        ja:{
            enterMuteWord: '"/"で括ると正規表現',
            inputButton: '追加',
            toggleMuteWordsList: 'ミュートワードリストの表示/非表示',
            excludeOwner: 'チャンネル所有者を除く',
            excludePaidMessage: 'スーパーチャットを除く',
            showMuteWordOnHover: 'ホバー時にミュートワードを表示',
            showMutedContentOnHover: 'ツールチップで本文を表示',
            selectionEnabled: '選択ポップアップを有効化',
            hideChatCompletely: 'ミュートしたチャットを非表示'
        },
        en:{
            enterMuteWord: 'Use "/" for RegExp.',
            inputButton: 'Add',
            toggleMuteWordsList: 'Show/Hide mute word list',
            excludeOwner: 'Exclude channel owner',
            excludePaidMessage: 'Exclude Super Chat',
            showMuteWordOnHover: 'Show mute word on hover',
            showMutedContentOnHover: 'Show content tooltip on hover',
            selectionEnabled: 'Enable selection popup',
            hideChatCompletely: 'Hide muted chat completely'
        }
    };
    var YTChatWordMute = {};
    (YTChatWordMute => {
        class Main {
            constructor() {
                this.muteWords = [];
                this.strings = {};
                this.config = {
                    excludeOwner: true,
                    excludePaidMessage: false,
                    showMuteWordOnHover: true,
                    showMutedContentOnHover: true,
                    hideChatCompletely: false,
                    selectionEnabled: false,
                    reverseList: true,
                    openList: true
                };
            }
            load() {
                let language = ((window.navigator.languages && window.navigator.languages[0]) || window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage).substring(0, 2);
                if(Object.keys(STRINGS).indexOf(language) != -1) {
                    this.strings = STRINGS[language];
                }else {
                    this.strings = STRINGS.en;
                }

                this.muteWords = JSON.parse(GM_getValue('muteWords', false)) || JSON.parse(localStorage.getItem('muteWord')) || [];
                this.config = JSON.parse(GM_getValue('config', false)) || this.config;

                let promise = new Promise(resolve => {
                    let intervalid = window.setInterval(() => {
                        if(document.querySelector('#chat #items')) {
                            window.clearTimeout(timeoutid);
                            window.clearInterval(intervalid);
                            resolve(true);
                        }
                    }, 100);
                    let timeoutid = window.setTimeout(() => {
                        window.clearInterval(intervalid);
                    }, TIMEOUT);
                });
                promise.then(() => {
                    YTChatWordMute.wordMute.load();
                    YTChatWordMute.selection.load();
                    YTChatWordMute.popup.load();
                });
            }
            save() {
                GM_setValue('muteWords', JSON.stringify(this.muteWords));
                GM_setValue('config', JSON.stringify(this.config));
            }
        }
        class WordMute {
            refreshStylesheet() {
                let style = document.getElementById('styleWordMute');
                if(!style) {
                    style = document.createElement('style');
                    style.id = 'styleWordMute';
                    style.type = 'text/css';
                    document.head.appendChild(style);
                }
                const CSS = [
                    '.muted #message, .muted #author-name, .muted #chat-badges { display: none; }',
                    '.muted yt-live-chat-author-chip.yt-live-chat-text-message-renderer { margin-right: 0px !important; }',
                    '.muted #author-photo { visibility: collapse; }',
                    '.muted #content::after { content: "[Muted]"; display:inline-block; opacity: .25; width: 100%; line-height: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
                    '.muted:hover #content::after { content: "[Muted] "attr(muteword); }'
                ];
                if(YTChatWordMute.main.config.hideChatCompletely) {
                    style.innerHTML = CSS.join('\n') + '\n.muted { display: none; }';
                }else {
                    style.innerHTML = CSS.join('\n');
                }
            }
            load() {
                this.refreshStylesheet();
                this.applyMuteAll();

                let observer = new MutationObserver((mutations) => {
                    let chatItems;
                    if(mutations.length == 1) {
                        chatItems = Array.from(mutations[0].addedNodes);
                    }else {
                        chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                    }
                    this.applyMute(chatItems);
                });
                let chat = document.querySelector('#chat #items');
                observer.observe(chat, {childList: true});
            }
            addMuteWord(muteWord) {
                YTChatWordMute.main.muteWords.push(muteWord);
                YTChatWordMute.main.save();
            }
            removeMuteWord(muteWord) {
                if(YTChatWordMute.main.muteWords.indexOf(muteWord) >= 0) {
                    YTChatWordMute.main.muteWords.splice(YTChatWordMute.main.muteWords.indexOf(muteWord), 1);
                }
                YTChatWordMute.main.save();
            }
            applyMuteAll() {
                let chatItems = Array.from(document.querySelectorAll('#chat #items > .yt-live-chat-item-list-renderer'));
                this.applyMute(chatItems);
            }
            applyMute(chatItems) {
                chatItems = chatItems.filter(node => node.tagName == 'YT-LIVE-CHAT-TEXT-MESSAGE-RENDERER' || node.tagName == 'YT-LIVE-CHAT-PAID-MESSAGE-RENDERER');
                chatItems.forEach(node => {
                    let content = node.querySelector('#content');
                    content.removeAttribute('muteword');
                    content.removeAttribute('title');
                    node.classList.remove('muted');
                });

                if(YTChatWordMute.main.config.excludePaidMessage) {
                     chatItems = chatItems.filter(node => node.tagName != 'YT-LIVE-CHAT-PAID-MESSAGE-RENDERER');
                }
                if(YTChatWordMute.main.config.excludeOwner) {
                     chatItems = chatItems.filter(node => !node.hasAttribute('author-is-owner'));
                }
                chatItems.forEach(node => {
                    let content = node.querySelector('#content');
                    let message = node.querySelector('#message');
                    /*console.log('message.innerHTML: "' + message.innerHTML + '"');*/
                    let text = message.innerHTML.replace(/\r?\n/g, '').replace(/<paper-tooltip.*\/paper-tooltip>/g, '').replace(/<[^<]*alt=\"([^"]+)\"[^>]*>/g, '$1');
                    /*console.log('text: "' + text + '"');*/
                    for(let i = 0; i < YTChatWordMute.main.muteWords.length; i++) {
                        let muteWord = YTChatWordMute.main.muteWords[i];
                        if(muteWord.substring(0, 1) == '/' && muteWord.substring(muteWord.length - 1) == '/') {
                            muteWord = muteWord.substring(1, muteWord.length - 1);
                        }else {
                            muteWord = muteWord.replace(/[-.*+^|[\]()?${}\\]/g, '\\$&');
                        }
                        let regex = new RegExp(muteWord, 'u');
                        if(regex.test(text)) {
                            /*console.log('match: "' + YTChatWordMute.main.muteWords[i] + '", text: "' + text + '"');*/
                            if(YTChatWordMute.main.config.showMuteWordOnHover) {
                                content.setAttribute('muteword', YTChatWordMute.main.muteWords[i]);
                            }
                            if(YTChatWordMute.main.config.showMutedContentOnHover) {
                                content.setAttribute('title', text);
                            }
                            node.classList.add('muted');
                            break;
                        }
                    }
                });
            }
        }
        class Selection {
            refreshStylesheet() {
                let style = document.getElementById('styleSelection');
                if(!style) {
                    style = document.createElement('style');
                    style.id = 'styleSelection';
                    style.type = 'text/css';
                    document.head.appendChild(style);
                }
                const CSS = [
                    '#selectionPopup {position: absolute; padding: 8px; background-color: var(--yt-spec-general-background-a); color: var(--yt-live-chat-primary-text-color, var(--yt-primary-text-color)); font-size: 1.2em; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); border-radius: 4px; cursor: pointer;}',
                    '#selectionPopup:hover { background-color: var(--yt-spec-general-background-b); }'
                ];
                style.innerHTML = CSS.join('\n');
            }
            load() {
                this.refreshStylesheet();
                this.addEventListener();
            }
            addEventListener() {
                document.documentElement.addEventListener('click', onClick, false);
                function onClick(e) {
                    if(YTChatWordMute.main.config.selectionEnabled) {
                        if(e.target.id == 'selectionPopup') {
                            let selectionPopup = e.target;
                            let selectionText = selectionPopup.getAttribute('muteWord');
                            selectionPopup.remove();
                            YTChatWordMute.popup.addMuteWord(selectionText);
                        }else if(e.target.classList.contains('yt-live-chat-text-message-renderer') || e.target.classList.contains('yt-live-chat-paid-message-renderer') || e.target.classList.contains('yt-live-chat-item-list-renderer')) {
                            let selectionPopup = document.getElementById('selectionPopup');
                            if(selectionPopup) selectionPopup.remove();
                            if(document.getSelection() && !document.getSelection().isCollapsed) {
                                let selectionText = document.getSelection().toString();
                                selectionPopup = document.createElement('div');
                                selectionPopup.id = 'selectionPopup';
                                selectionPopup.style.top = e.pageY + 'px';
                                selectionPopup.style.left = e.pageX + 'px';
                                selectionPopup.textContent = 'Mute "' + selectionText + '"';
                                selectionPopup.setAttribute('muteWord', selectionText);
                                document.body.appendChild(selectionPopup);
                            }
                        }
                    }else {
                        document.documentElement.removeEventListener('click', onClick, false);
                    }
                }
            }
        }
        class Popup {
            refreshStylesheet() {
                let style = document.getElementById('stylePopup');
                if(!style) {
                    style = document.createElement('style');
                    style.id = 'stylePopup';
                    style.type = 'text/css';
                    document.head.appendChild(style);
                }
                const CSS = [
                    '#muteMenu { display: inline-block; position: relative; width: 40px; height: 40px; text-align: center; line-height: 40px; font-size: 0.9em; color: var(--yt-spec-icon-inactive); cursor: pointer; }',
                    '#muteMenu:hover, #muteMenu[active="true"] { color: var(--yt-spec-icon-active-other); }',
                    '#muteMenu::after { content: ""; display: block; position: absolute; width: 0px; height: 0px; left: 20px; top: 20px; border-radius: 50%; background-color: var(--yt-spec-icon-active-other); opacity: .25; transition-duration: .2s; }',
                    '#muteMenu[active="true"]::after { width: 40px; height: 40px; left: 0px; top: 0px; }',
                    '#mutePopupContainer { position: fixed; width: 220px; top: 44px; right: 14px; padding: 12px; background-color: var(--yt-spec-brand-background-solid); color: var(--paper-listbox-color, #212121); box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); border-radius: 4px; }',
                    '#muteWordInput { width: 100%; border: none; outline: none; background-color: transparent; color: var(--yt-live-chat-primary-text-color); }',
                    '#muteWordInput::placeholder { color: var(--yt-live-chat-text-input-field-placeholder-color, hsla(0, 0%, 6.7%, 0.6)); }',
                    '#muteWordInput + #underline { position: relative; height: 2px; }',
                    '#muteWordInput + #underline #unfocused { position: absolute; top: 0; left: 0; right: 0; height: 1px; background-color: var(--yt-live-chat-text-input-field-inactive-underline-color, #b8b8b8); }',
                    '#muteWordInput + #underline #focused { position: absolute; top: 0; left: 0; right: 0; height: 2px; background-color: var(--yt-live-chat-text-input-field-active-underline-color, #2793e6); transform-origin: center center; transform: scale3d(0,1,1); transition: transform; transition-duration: 0.25s; }',
                    '#muteWordInput:focus + #underline #focused { transform: none; }',
                    '#muteWordAdd { display: block; margin: 8px 0 0 auto; padding: 8px; border: none; background-color: transparent; outline: none; font-size: 1.4rem; font-weight: 500; letter-spacing: .007px; color: var(--yt-spec-call-to-action); cursor: pointer; }',
                    '.openButton { margin-top: 8px; padding: 4px; background-color: var(--yt-spec-general-background-a); border-radius: 4px; text-align: center; cursor: pointer; }',
                    '.openButton:hover {background-color: var(--yt-spec-general-background-b); }',
                    '.listContainer { padding: 0; margin: 0; max-height: 270px; overflow: auto; }',
                    '.listContainer:not([empty="true"]) { margin-top: 12px; }',
                    '#muteWordListContainer .listContainer:not([open]) { display: none; }',
                    '.listItem { display: flex; font-size: 1.2em; line-height: 18px; }',
                    '.listItem:nth-child(even) { background-color: var(--yt-spec-general-background-a); }',
                    '.title { flex: 1; padding-left: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
                    '.removeButton { width: 16px; cursor: pointer }',
                    '.removeButton:hover { text-decoration: underline; }'
                ];
                style.innerHTML = CSS.join('\n');
            }
            load() {
                this.refreshStylesheet();

                let menu = document.querySelector('#overflow');
                let muteMenu = document.createElement('a');
                muteMenu.id = 'muteMenu';
                muteMenu.innerText = 'Mute';
                menu.parentNode.insertBefore(muteMenu, menu);
                muteMenu.addEventListener('click', () => {
                    this.toggle();
                }, false);

                let mutePopupContainer = document.createElement('div');
                mutePopupContainer.id = 'mutePopupContainer';
                mutePopupContainer.className = 'hidden';
                let addMuteWordContainer = document.createElement('div');
                addMuteWordContainer.id = 'addMuteWordContainer';
                let inputText = document.createElement('input');
                inputText.id = 'muteWordInput';
                inputText.type = 'text';
                inputText.setAttribute('autocomplete', 'off');
                inputText.placeholder = YTChatWordMute.main.strings.enterMuteWord;
                addMuteWordContainer.appendChild(inputText);
                inputText.addEventListener('keypress', (e) => {
                    if(e.keyCode == '13') {
                        this.addMuteWord();
                    }
                }, false);
                let underline = document.createElement('div');
                underline.id = 'underline';
                let unfocused = document.createElement('div');
                unfocused.id = 'unfocused';
                underline.appendChild(unfocused);
                let focused = document.createElement('div');
                focused.id = 'focused';
                underline.appendChild(focused);
                addMuteWordContainer.appendChild(underline);
                let inputButton = document.createElement('input');
                inputButton.id = 'muteWordAdd';
                inputButton.type = 'button';
                inputButton.value = YTChatWordMute.main.strings.inputButton;
                addMuteWordContainer.appendChild(inputButton);
                inputButton.addEventListener('click', () => {
                    this.addMuteWord();
                }, false);

                let muteWordListContainer = document.createElement('div');
                muteWordListContainer.id = 'muteWordListContainer';
                let muteWordList = document.createElement('ul');
                muteWordList.id = 'muteWordList';
                muteWordList.className = 'listContainer';
                muteWordList.setAttribute('empty', 'true');
                if(YTChatWordMute.main.config.openList) {
                    muteWordList.setAttribute('open', 'true');
                }else {
                    muteWordList.removeAttribute('open');
                }
                let openButton = document.createElement('div');
                openButton.className = 'openButton';
                openButton.textContent = YTChatWordMute.main.strings.toggleMuteWordsList;
                muteWordListContainer.appendChild(openButton);
                openButton.addEventListener('click', () => {
                    YTChatWordMute.main.config.openList = !YTChatWordMute.main.config.openList;
                    YTChatWordMute.main.save();
                    if(YTChatWordMute.main.config.openList) {
                        muteWordList.setAttribute('open', 'true');
                    }else {
                        muteWordList.removeAttribute('open');
                    }
                }, false);
                muteWordListContainer.appendChild(muteWordList);

                let SettingContainer = document.createElement('div');
                SettingContainer.id = 'SettingContainer';
                let SettingList = document.createElement('ul');
                SettingList.id = 'SettingList';
                SettingList.className = 'listContainer';
                SettingList.setAttribute('empty', 'false');
                SettingContainer.appendChild(SettingList);

                let listItem, title, checkBox;
                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.excludeOwner;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.excludeOwner;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.excludeOwner = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.wordMute.applyMuteAll();
                }, false);

                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.excludePaidMessage;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.excludePaidMessage;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.excludePaidMessage = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.wordMute.applyMuteAll();
                }, false);

                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.showMuteWordOnHover;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.showMuteWordOnHover;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.showMuteWordOnHover = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.wordMute.applyMuteAll();
                }, false);

                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.showMutedContentOnHover;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.showMutedContentOnHover;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.showMutedContentOnHover = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.wordMute.applyMuteAll();
                }, false);

                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.selectionEnabled;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.selectionEnabled;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.selectionEnabled = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.selection.addEventListener();
                }, false);

                listItem = document.createElement('li');
                listItem.className = 'listItem';
                title = document.createElement('span');
                title.className = 'title';
                title.textContent = YTChatWordMute.main.strings.hideChatCompletely;
                checkBox = document.createElement('input');
                checkBox.type = 'checkbox';
                checkBox.checked = YTChatWordMute.main.config.hideChatCompletely;
                listItem.appendChild(title);
                listItem.appendChild(checkBox);
                SettingList.appendChild(listItem);
                checkBox.addEventListener('click', (e) => {
                    YTChatWordMute.main.config.hideChatCompletely = e.target.checked;
                    YTChatWordMute.main.save();
                    YTChatWordMute.wordMute.refreshStylesheet();
                }, false);

                mutePopupContainer.appendChild(addMuteWordContainer);
                mutePopupContainer.appendChild(muteWordListContainer);
                mutePopupContainer.appendChild(SettingContainer);
                document.body.appendChild(mutePopupContainer);

                YTChatWordMute.main.muteWords.forEach(muteWord => {
                    this.addListItem(muteWord);
                });

                document.body.addEventListener('click', (e) => {
                    let mutePopupContainer = document.querySelector('#mutePopupContainer');
                    let muteMenu = document.querySelector('#muteMenu');
                    let target = e.target;
                    if(!mutePopupContainer.classList.contains('hidden') && target != muteMenu && target.className != 'removeButton') {
                        while (target && (target != document.body)) {
                            if (target == mutePopupContainer) return;
                            target = target.parentNode;
                        }
                        this.toggle();
                    }
                }, false);
            }
            addMuteWord(muteWord) {
                let inputText = document.querySelector('#muteWordInput');
                if(!muteWord) muteWord = inputText.value;
                if(muteWord !== '' && YTChatWordMute.main.muteWords.indexOf(muteWord) == -1) {
                    YTChatWordMute.wordMute.addMuteWord(muteWord);
                    this.addListItem(muteWord);
                    inputText.value = '';
                    YTChatWordMute.wordMute.applyMuteAll();
                }
            }
            toggle() {
                let mutePopupContainer = document.querySelector('#mutePopupContainer');
                let muteMenu = document.querySelector('#muteMenu');
                if(mutePopupContainer.classList.contains('hidden')) {
                    mutePopupContainer.classList.remove('hidden');
                    let inputText = document.querySelector('#muteWordInput');
                    inputText.focus();
                    muteMenu.setAttribute('active', 'true');
                }else {
                    mutePopupContainer.classList.add('hidden');
                    muteMenu.removeAttribute('active');
                }
            }
            addListItem(muteWord) {
                let parent = document.querySelector('#muteWordList');
                let listItem = document.createElement('li');
                listItem.setAttribute('muteword', muteWord);
                listItem.className = 'listItem';
                let title = document.createElement('span');
                title.className = 'title';
                title.title = muteWord;
                title.textContent = muteWord;
                let removeButton = document.createElement('a');
                removeButton.setAttribute('muteword', muteWord);
                removeButton.className = 'removeButton';
                removeButton.textContent = '[x]';
                listItem.appendChild(title);
                listItem.appendChild(removeButton);
                if(YTChatWordMute.main.config.reverseList) {
                    parent.insertBefore(listItem, parent.firstChild);
                }else {
                    parent.appendChild(listItem);
                }

                removeButton.addEventListener('click', (e) => {
                    YTChatWordMute.wordMute.removeMuteWord(muteWord);
                    this.removeListItems(e.target.getAttribute('muteword'));
                    YTChatWordMute.wordMute.applyMuteAll();
                }, false);
                parent.setAttribute('empty', 'false');
            }
            removeListItems(muteWord) {
                let listItems = Array.from(document.querySelectorAll('.listItem'));
                listItems.forEach(item => {
                    if(muteWord == item.getAttribute('muteword')) {
                        item.remove();
                    }
                });
                if(listItems.length == 1) {
                    let parent = document.querySelector('#muteWordList');
                    parent.setAttribute('empty', 'true');
                }
            }
        }
        YTChatWordMute.main = new Main();
        YTChatWordMute.wordMute = new WordMute();
        YTChatWordMute.selection = new Selection();
        YTChatWordMute.popup = new Popup();
    })(YTChatWordMute);

    YTChatWordMute.main.load();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址