Block Twitch Emotes - with button Block emote.

Twitch emoji blocking via a channel with a management interface and context menu (via Tampermonkey)

目前为 2024-12-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         Block Twitch Emotes - with button Block emote.
// @namespace    http://tampermonkey.net/
// @version      1.7.0
// @description  Twitch emoji blocking via a channel with a management interface and context menu (via Tampermonkey)
// @author
// @license      MIT
// @match        https://www.twitch.tv/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://cdn-icons-png.flaticon.com/512/10531/10531738.png
// ==/UserScript==

(function () {
    'use strict';

    let blockedChannels = GM_getValue('blockedChannels', []);
    let isPanelVisible = GM_getValue('isPanelVisible', false);

    const controlPanel = document.createElement('div');
    controlPanel.style.position = 'fixed';
    controlPanel.style.bottom = '124px';
    controlPanel.style.left = '1210px';
    controlPanel.style.width = '360px';
    controlPanel.style.backgroundColor = '#5c5065';
    controlPanel.style.border = '1px solid #ccc';
    controlPanel.style.borderRadius = '8px';
    controlPanel.style.padding = '10px';
    controlPanel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
    controlPanel.style.zIndex = 10000;
    controlPanel.style.fontFamily = 'Arial, sans-serif';

    const title = document.createElement('h4');
    title.innerText = 'Block Twitch Emotes';
    title.style.margin = '0 0 10px 0';
    title.style.color = '#fff';
    controlPanel.appendChild(title);

    // Кнопка "Закрыть"
    const closeButton = document.createElement('button');
    closeButton.innerText = 'Close';
    closeButton.style.background = '#DC3545';
    closeButton.style.color = '#fff';
    closeButton.style.border = '1px solid #ccc';
    closeButton.style.width = '55px'; // Фиксированная ширина кнопки
    closeButton.style.height = '30px'; // Фиксированная высота кнопки
    closeButton.style.borderRadius = '8px';
    closeButton.style.padding = '5px 10px';
    closeButton.style.cursor = 'pointer';
    closeButton.style.marginTop = '10px';
    closeButton.onclick = () => {
        isPanelVisible = false;
        GM_setValue('isPanelVisible', isPanelVisible);
        controlPanel.style.display = 'none'; // Скрыть панель
    };
    controlPanel.appendChild(closeButton);

const list = document.createElement('ul');
list.id = 'blockedChannelsList';
list.style.listStyle = 'none';
list.style.padding = '0';
list.style.margin = '0 0 10px 0';
list.style.maxHeight = '230px';
list.style.height = '550px';
list.style.overflowY = 'auto';
list.style.border = '1px solid #ddd';
list.style.borderRadius = '4px';

// Применяем новый градиентный фон
list.style.background = 'linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)';
list.style.background = '-moz-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)'; // Для Firefox
list.style.background = '-webkit-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)'; // Для Safari и Chrome
list.style.filter = 'progid: DXImageTransform.Microsoft.gradient(startColorstr="#36173b", endColorstr="#589F97", GradientType=1)'; // Для старых версий IE

list.style.color = '#fff';

    function updateChannelList() {
        list.innerHTML = '';
        blockedChannels.forEach(channel => {
            const item = document.createElement('li');
            item.style.display = 'flex';
            item.style.justifyContent = 'space-between';
            item.style.padding = '5px';
            item.style.borderBottom = '1px solid #eee';

            const channelName = document.createElement('span');
            channelName.innerText = channel;
            item.appendChild(channelName);

            const removeButton = document.createElement('button');
            removeButton.innerText = 'Delete channel';
            removeButton.style.background = '#ff4d4d';
            removeButton.style.color = '#fff';
            removeButton.style.border = 'none';
            removeButton.style.borderRadius = '4px';
            removeButton.style.padding = '2px 6px';
            removeButton.style.cursor = 'pointer';
            removeButton.onclick = function () {
                blockedChannels = blockedChannels.filter(c => c !== channel);
                GM_setValue("blockedChannels", blockedChannels);
                updateChannelList();
                updateCounter();
            };
            item.appendChild(removeButton);

            list.appendChild(item);
        });
    }

    controlPanel.appendChild(list);

    const inputContainer = document.createElement('div');
    inputContainer.style.display = 'flex';
    inputContainer.style.gap = '5px';

    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'type channel name';
    input.style.flex = '1';
    input.style.padding = '5px';
    input.style.border = '1px solid #ccc';
    input.style.borderRadius = '4px';

    const addButton = document.createElement('button');
    addButton.innerText = 'Add it';
    addButton.style.background = '#4CAF50';
    addButton.style.color = '#fff';
    addButton.style.border = 'none';
    addButton.style.borderRadius = '4px';
    addButton.style.padding = '5px 10px';
    addButton.style.cursor = 'pointer';
    addButton.onclick = (event) => {
        event.preventDefault();
        const channel = input.value.trim();
        if (channel && !blockedChannels.includes(channel)) {
            blockedChannels.push(channel);
            GM_setValue("blockedChannels", blockedChannels);
            updateChannelList();
            updateCounter();
            input.value = '';
        }
    };

    const buttonContainer = document.createElement('div');
    buttonContainer.style.display = 'flex';
    buttonContainer.style.gap = '6px';
    buttonContainer.style.marginTop = '10px';

    const clearAllButton = document.createElement('button');
    clearAllButton.innerText = 'Delete all';
    clearAllButton.style.background = '#DC3545';
    clearAllButton.style.color = '#fff';
    clearAllButton.style.border = 'none';
    clearAllButton.style.borderRadius = '4px';
    clearAllButton.style.padding = '5px 10px';
    clearAllButton.style.cursor = 'pointer';
    buttonContainer.appendChild(clearAllButton);
    clearAllButton.onclick = () => {
        blockedChannels = [];
        GM_setValue("blockedChannels", blockedChannels);
        updateChannelList();
        updateCounter();
    };

    const exportButton = document.createElement('button');
    exportButton.innerText = 'Export';
    exportButton.style.background = '#007BFF';
    exportButton.style.color = '#fff';
    exportButton.style.border = 'none';
    exportButton.style.borderRadius = '4px';
    exportButton.style.padding = '5px 10px';
    exportButton.style.cursor = 'pointer';
    buttonContainer.appendChild(exportButton);
    exportButton.onclick = () => {
        const blob = new Blob([JSON.stringify(blockedChannels, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = 'blocked_channels.json';
        link.click();
        URL.revokeObjectURL(url);
    };

    const importButton = document.createElement('button');
    importButton.innerText = 'Import';
    importButton.style.background = '#28A745';
    importButton.style.color = '#fff';
    importButton.style.border = 'none';
    importButton.style.borderRadius = '4px';
    importButton.style.padding = '5px 10px';
    importButton.style.cursor = 'pointer';
    buttonContainer.appendChild(importButton);
    importButton.onclick = () => {
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = 'application/json';
        fileInput.onchange = (event) => {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const importedChannels = JSON.parse(e.target.result);
                    if (Array.isArray(importedChannels)) {
                        blockedChannels = Array.from(new Set([...blockedChannels, ...importedChannels]));
                        GM_setValue("blockedChannels", blockedChannels);
                        updateChannelList();
                        updateCounter();
                    } else {
                        alert('Invalid file format!');
                    }
                } catch {
                    alert('Error reading file!');
                }
            };
            reader.readAsText(file);
        };
        fileInput.click();
    };

    // Счётчик
    const counter = document.createElement('div');
    counter.style.display = 'inline-block';
    counter.style.backgroundColor = '#fff';
    counter.style.color = '#4c2a5e';
    counter.style.border = '1px solid #ccc';
    counter.style.borderRadius = '8px';
    counter.style.padding = '5px 10px';
    counter.style.marginLeft = '6px';
    counter.style.fontWeight = 'bold';
    buttonContainer.appendChild(counter);

    function updateCounter() {
        counter.innerText = `Count: ${blockedChannels.length}`;
    }

    inputContainer.appendChild(input);
    inputContainer.appendChild(addButton);
    controlPanel.appendChild(inputContainer);
    controlPanel.appendChild(buttonContainer);

    document.body.appendChild(controlPanel);

    function hideEmotesForChannel(node) {
        const emotes = node.querySelectorAll('.chat-line__message img');
        emotes.forEach(emote => {
            const emoteName = emote.getAttribute('alt');
            if (emoteName && isEmoteFromBlockedChannel(emoteName)) {
                emote.style.display = 'none';
            }
        });
    }

    function isEmoteFromBlockedChannel(emoteName) {
        return blockedChannels.some(channel => emoteName.includes(channel));
    }

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) hideEmotesForChannel(node);
            });
        });
    });


// Создаем кнопку "Open Blocker Emote"
const openPanelButton = document.createElement('button');
openPanelButton.innerText = 'Open Blocker Emote';
openPanelButton.style.fontWeight = 'bold';
openPanelButton.style.position = 'fixed'; // Фиксированное положение
openPanelButton.style.width = '200px'; // Фиксированная ширина кнопки
openPanelButton.style.height = '41px'; // Фиксированная высота кнопки
openPanelButton.style.background = '#4c2a5e'; // Цвет кнопки в выключенном состоянии
openPanelButton.style.color = '#fff';
openPanelButton.style.border = 'none'; // Без границ
openPanelButton.style.borderRadius = '20px'; // Закругленные углы
openPanelButton.style.padding = '10px';
openPanelButton.style.cursor = 'pointer';
openPanelButton.style.zIndex = 10000; // Высокий z-index
openPanelButton.style.transition = 'background 0.3s ease'; // Плавное изменение фона
openPanelButton.style.display = 'flex';
openPanelButton.style.alignItems = 'center';
openPanelButton.style.justifyContent = 'space-between'; // Чтобы текст и переключатель были по разным краям

// Создаем контейнер для переключателя (темная рамка)
const switchContainer = document.createElement('div');
switchContainer.style.width = '44px'; // Увеличиваем ширину контейнера на 6px
switchContainer.style.height = '27px'; // Увеличиваем высоту контейнера на 6px
switchContainer.style.borderRadius = '13px'; // Скругленные углы
switchContainer.style.backgroundColor = '#9078a7'; // Темно-зеленая рамка для кружка
switchContainer.style.position = 'relative'; // Для абсолютного позиционирования кружка
switchContainer.style.transition = 'background 0.3s ease'; // Плавное изменение фона контейнера
openPanelButton.appendChild(switchContainer);

// Создаем фиолетовый кружок (переключатель)
const switchCircle = document.createElement('div');
switchCircle.style.width = '19px'; // Увеличиваем ширину кружка на 3px
switchCircle.style.height = '19px'; // Увеличиваем высоту кружка на 3px
switchCircle.style.borderRadius = '50%'; // Кружок
switchCircle.style.backgroundColor = '#4c2a5e'; // фиолетовый цвет кружка
switchCircle.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.8)'; // Тень для кружка
switchCircle.style.position = 'absolute'; // Абсолютное позиционирование внутри контейнера
switchCircle.style.top = '3px'; // Отступ сверху
switchCircle.style.left = '3px'; // Отступ слева
switchCircle.style.transition = 'transform 0.3s ease'; // Плавное движение
switchContainer.appendChild(switchCircle);

// Устанавливаем начальное состояние
let isPanelOpen = false;

// Обработчик клика для изменения состояния
openPanelButton.onclick = () => {
    isPanelOpen = !isPanelOpen;
    if (isPanelOpen) {
        openPanelButton.style.background = '#4c2a5e'; // Включено
        switchCircle.style.transform = 'translateX(20px)'; // Перемещаем кружок вправо
        switchContainer.style.backgroundColor = '#3c995d'; // Цвет контейнера в включенном состоянии
        controlPanel.style.display = 'block'; // Показать панель
    } else {
        openPanelButton.style.background = '#4c2a5e'; // Выключено
        switchCircle.style.transform = 'translateX(0)'; // Перемещаем кружок влево
        switchContainer.style.backgroundColor = '#907cad'; // Цвет контейнера в выключенном состоянии
        controlPanel.style.display = 'none'; // Скрыть панель
    }
    GM_setValue('isPanelOpen', isPanelOpen); // Сохранить состояние
};

// Добавляем кнопку в DOM только после полной загрузки страницы
window.addEventListener('load', () => {
    document.body.appendChild(openPanelButton);

    // Устанавливаем начальное положение кнопки
    const updateButtonPosition = () => {
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;

        // Позиция кнопки (например, 5% от высоты и 10% от ширины)
        openPanelButton.style.top = `${windowHeight * 0.01}px`; // 1% от высоты окна
        openPanelButton.style.right = `${windowWidth * 0.2}px`; // 20% от ширины окна
    };

    // Устанавливаем положение кнопки сразу
    updateButtonPosition();

    // Обновляем положение кнопки при изменении размеров окна
    window.addEventListener('resize', updateButtonPosition);
});






    function waitForChatContainer() {
        const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
        if (chatContainer) observer.observe(chatContainer, { childList: true, subtree: true });
        else setTimeout(waitForChatContainer, 100);
    }

    waitForChatContainer();

    // Periodic check to hide emotes that may have been re-rendered
    setInterval(() => {
        const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
        if (chatContainer) {
            const nodes = chatContainer.querySelectorAll('.chat-line__message');
            nodes.forEach(node => hideEmotesForChannel(node));
        }
    }, 100); // Checking every second

    GM_registerMenuCommand('Toggle Block Emotes Panel', () => {
        isPanelVisible = !isPanelVisible;
        GM_setValue('isPanelVisible', isPanelVisible);
        controlPanel.style.display = isPanelVisible ? 'block' : 'none';
    });

    if (isPanelVisible) controlPanel.style.display = 'block';
    else controlPanel.style.display = 'none';

    updateChannelList();
    updateCounter();

    // Добавляем контекстное меню
    document.addEventListener('contextmenu', (event) => {
        const target = event.target;
        if (target.tagName === 'IMG' && target.closest('.chat-line__message')) {
            event.preventDefault();

            const emoteAlt = target.getAttribute('alt');
            if (emoteAlt) {
                const emotePrefix = emoteAlt.split(/[^a-zA-Z0-9]/)[0];

                const contextMenu = document.createElement('div');
                contextMenu.style.position = 'absolute';
                contextMenu.style.top = `${event.pageY}px`;
                contextMenu.style.left = `${event.pageX}px`;
                contextMenu.style.background = '#4c2a5e';
                contextMenu.style.border = '1px solid #ccc';
                contextMenu.style.padding = '5px';
                contextMenu.style.zIndex = 10001;
                contextMenu.style.cursor = 'pointer';
                contextMenu.innerText = `Block Emote (${emotePrefix})`;

                document.body.appendChild(contextMenu);

                contextMenu.addEventListener('click', () => {
                    if (!blockedChannels.includes(emotePrefix)) {
                        blockedChannels.push(emotePrefix);
                        GM_setValue("blockedChannels", blockedChannels);
                        updateChannelList();
                        updateCounter();
                    }
                    contextMenu.remove();
                });

                document.addEventListener('click', () => contextMenu.remove(), { once: true });
            }
        }
    });
})();