您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Twitch emoji blocking in Chat with a management interface and context menu .
当前为
// ==UserScript== // @name Control Emotes Panel 2.6.52 (C) tapeavion // @version 2.6.52 // @description Twitch emoji blocking in Chat with a management interface and context menu . // @author Gullampis810 // @license Proprietary (c) tapeavion // @match www.twitch.tv/* // @match https://blank.org/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @connect raw.githubusercontent.com // @grant GM_xmlhttpRequest // @icon https://raw.githubusercontent.com/sopernik566/icons/refs/heads/main/7bttvffzTtvlogo.ico // @namespace http://tampermonkey.net/ // @tag twitch // @tag emote-blocker // @tag chat-filter // @tag block-emotes // @tag twitch-chat // @tag 7tv // @tag bttv // @tag ffz // @tag twitch-channel // @tag block all emotes // ==/UserScript== (function () { 'use strict'; console.log("Скрипт запущен в чате"); // Подключаем Chart.js через CDN const chartJsScript = document.createElement('script'); chartJsScript.src = 'https://cdn.jsdelivr.net/npm/chart.js'; chartJsScript.async = true; document.head.appendChild(chartJsScript); // === объект для хранения текущих стилей кнопки DeleteButton в случае пересоздания и сброса стиля // let currentDeleteButtonStyles = { background: 'rgb(168, 77, 77)', // Начальный цвет из hoverStyle color: ' #fff', hoverBackground: 'linear-gradient(135deg, #f75557 0%, #480a0c 56%, #4e1314 98%, #ff4d4d 100%)' }; let blockedEmotes = []; let blockedChannels = []; // глобальное определение для поисковой строки function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Функция для безопасного получения и парсинга данных function loadData(key, defaultValue) { const rawData = GM_getValue(key, defaultValue); try { return typeof rawData === 'string' ? JSON.parse(rawData) : rawData; } catch (e) { console.error(`Ошибка при парсинге ${key}:`, e); return defaultValue; // Возвращаем значение по умолчанию в случае ошибки } } // Загружаем данные при старте blockedEmotes = loadData("blockedEmotes", []); blockedChannels = loadData("blockedChannels", []); console.log("[7BTTVFZ Control Emotes Panel] Загружены blockedEmotes:", blockedEmotes); console.log("[7BTTVFZ Control Emotes Panel] Загружены blockedChannels:", blockedChannels); let isPanelOpen = GM_getValue('isPanelOpen', false); //=== Функция для перемещения панели ===// function makePanelDraggable(panel) { let offsetX = 0, offsetY = 0, isDragging = false; // Создаем заголовок, за который можно перетаскивать const dragHandle = document.createElement('div'); dragHandle.style.width = '100%'; dragHandle.style.height = '656px'; dragHandle.style.background = 'rgba(0, 0, 0, 0.0)'; dragHandle.style.cursor = 'grab'; dragHandle.style.position = 'absolute'; dragHandle.style.top = '0'; dragHandle.style.left = '0'; dragHandle.style.zIndex = '-1'; dragHandle.style.borderRadius = '8px 8px 0 0'; panel.appendChild(dragHandle); // Начало перемещения dragHandle.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; dragHandle.style.cursor = 'grabbing'; }); // Перемещение панели document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = `${e.clientX - offsetX}px`; panel.style.top = `${e.clientY - offsetY}px`; }); // Остановка перемещения document.addEventListener('mouseup', () => { isDragging = false; dragHandle.style.cursor = 'grab'; }); } //===================================== Панель управления =======================================// const controlPanel = document.createElement('div'); controlPanel.style.position = 'fixed'; // Фиксируем панель на экране controlPanel.style.bottom = '124px'; // Располагаем панель на 124px от нижней границы экрана controlPanel.style.right = '380px'; // Располагаем панель на 310px от правой границы экрана controlPanel.style.width = '690px'; // Ширина панели controlPanel.style.height = '656px'; // Высота панели controlPanel.style.backgroundColor = ' #5c5065'; // Цвет фона панели controlPanel.style.background = '-webkit-linear-gradient(270deg, hsla(50, 76%, 56%, 1) 0%, hsla(32, 83%, 49%, 1) 25%, hsla(0, 37%, 37%, 1) 59%, hsla(276, 47%, 24%, 1) 79%, hsla(261, 11%, 53%, 1) 100%)'; // Применяем градиентный фон controlPanel.style.border = '1px solid #ccc'; // Цвет и стиль границы панели controlPanel.style.borderRadius = '8px'; // Скругляем углы панели controlPanel.style.padding = '10px'; // Отступы внутри панели controlPanel.style.boxShadow = 'rgb(0 0 0) 0px 18px 29px 3px'; // Добавляем тень панели controlPanel.style.zIndex = 10000; // Устанавливаем высокий z-index, чтобы панель была поверх других элементов controlPanel.style.fontFamily = 'Arial, sans-serif'; // Шрифт текста на панели controlPanel.style.transition = 'height 0.3s ease'; // Плавное изменение высоты при изменении controlPanel.style.overflow = 'hidden'; // Скрытие содержимого, если оно выходит за пределы панели // Метка версии внизу панели const versionLabel = document.createElement('div'); versionLabel.innerText = 'v.2.6.52'; versionLabel.style.position = 'absolute'; versionLabel.style.top = '4px'; versionLabel.style.right = '610px'; versionLabel.style.color = ' #3e2155'; versionLabel.style.fontSize = '12px'; versionLabel.style.fontFamily = 'Arial, sans-serif'; versionLabel.style.fontWeight = 'bold'; controlPanel.appendChild(versionLabel); // Добавляем панель в DOM и активируем перетаскивание document.body.appendChild(controlPanel); makePanelDraggable(controlPanel); // ======== Создаём кнопку шестерёнки для настроек ======== // const settingsButton = document.createElement('button'); settingsButton.innerHTML = '<img src="https://raw.githubusercontent.com/sopernik566/Control_Emotes_Panel_Twitch_JS/dec21031f3f5d70d26b43c11ab4cb5b401b6c67d/settingsbttn7btttvttvffz.png" alt="Settings" style="width: 28px; height: 28px; vertical-align: middle; background: #5d5d5d; border-radius: 8px; border: 1px solid #333333;">'; settingsButton.style.width = '28px'; settingsButton.style.height = '28px'; settingsButton.style.position = 'absolute'; settingsButton.style.top = '3px'; settingsButton.style.right = '657px'; settingsButton.style.background = 'transparent'; settingsButton.style.border = 'none'; settingsButton.style.color = ' #fff'; settingsButton.style.cursor = 'pointer'; settingsButton.style.zIndex = '10001'; settingsButton.title = getTranslation('settingsTitle'); controlPanel.appendChild(settingsButton); // Добавляем CSS для попапа и анимации const styleSheet = document.createElement('style'); styleSheet.textContent = ` .settings-popup { position: fixed; top: 20%; left: 30%; width: 40%; height: 60%; background: linear-gradient(181deg, hsla(50, 76%, 56%, 1) 0%, hsla(0, 37%, 37%, 1) 59%, hsla(276, 47%, 24%, 1) 79%); border: 1px solid #bda3d7; border-radius: 12px; z-index: 10002; padding: 10px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; box-shadow: 6px 12px 20px 9px rgb(0 0 0 / 74%); transform: translateY(-100%); opacity: 0; } .popup-enter { animation: slideDown 0.3s ease-in-out forwards; } .popup-exit { animation: slideUp 0.3s ease-in-out forwards; } @keyframes slideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideUp { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-100%); opacity: 0; } } .settings-popup .close-button { position: absolute; top: 8px; right: 8px; background: #2f213b; color: #fff; border: none; border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .settings-popup .settings-title { margin: 0 0 12px 0; font-size: 18px; color: #2f213b; font-weight: bold; } .settings-popup .settings-content { font-size: 14px; color: #fff; } .settings-popup .settings-description { margin: 0 0 12px 0; color: #2f213b; } .settings-popup .watchdog-section, .settings-popup .language-section, .settings-popup .font-size-section { margin-top: 15px; } .settings-popup .watchdog-label, .settings-popup .language-label, .settings-popup .font-size-label { display: block; margin-bottom: 6px; font-size: 14px; color: #2f213b; } .settings-popup .watchdog-interval, .settings-popup .language-select, .settings-popup .font-size-input { width: 125px; padding: 6px; border-radius: 6px; border: 1px solid #bda3d7; background: rgb(47, 33, 59); color: #bda3d7; font-size: 14px; } .settings-popup .language-select { width: 125px; } `; document.head.appendChild(styleSheet); // Функция для получения переводов function getTranslation(key, lang = GM_getValue('language', 'ru')) { const translations = { ru: { settingsFailed: 'Не удалось загрузить настройки', watchdogUpdated: 'Интервал watchdog обновлён до {interval}с', watchdogInvalid: 'Интервал должен быть минимум 1 секунда', languageUpdated: 'Язык интерфейса обновлён', closeButton: 'Закрыть', settingsTitle: 'Настройки', settingsDescription: 'Настройте параметры панели', watchdogLabel: 'Интервал проверки Watchdog (секунды):', languageLabel: 'Язык интерфейса:', blockedEmotesList: 'Список заблокированных эмодзи', platformLabel: 'Платформа', nameLabel: 'Имя', dateTimeLabel: 'Дата-Время', goToLastElement: 'Перейти к последнему элементу', showStatsChart: 'Показать статистику', deleteAll: 'Удалить все', export: 'Экспорт', import: 'Импорт', unblockAll: 'Разблокировать все эмодзи', blockAll: 'Заблокировать все эмодзи', searchPlaceholder: 'Поиск в списке заблокированных…', searchButton: 'Поиск', twitchChannelLabel: 'Twitch-канал', addChannelPlaceholder: 'Введите канал для добавления', addChannelButton: 'Добавить' }, en: { settingsFailed: 'Failed to load settings', watchdogUpdated: 'Watchdog interval updated to {interval}s', watchdogInvalid: 'Interval must be at least 1 second', languageUpdated: 'Interface language updated', closeButton: 'Close', settingsTitle: 'Settings', settingsDescription: 'Configure panel settings', watchdogLabel: 'Watchdog check interval (seconds):', languageLabel: 'Interface language:', blockedEmotesList: 'List of Blocked Emotes', platformLabel: 'Platform', nameLabel: 'Name', dateTimeLabel: 'Date-Time', goToLastElement: 'Go to Last Element', showStatsChart: 'Show Stats Chart', deleteAll: 'Delete All', export: 'Export', import: 'Import', unblockAll: 'Unblock All Emotes', blockAll: 'Block All Emotes', searchPlaceholder: 'Search in blocked list…', searchButton: 'Search', twitchChannelLabel: 'Twitch Channel', addChannelPlaceholder: 'Type to add channel', addChannelButton: 'Add it' } }; return translations[lang][key] || translations.ru[key]; } // Функция для обновления переводов панели function updatePanelTranslations(lang = GM_getValue('language', 'ru')) { const blockedEmotesList = document.querySelector('.blocked-emotes-list'); if (blockedEmotesList) { blockedEmotesList.textContent = getTranslation('blockedEmotesList', lang); } const platformLabel = document.querySelector('.platform-label'); if (platformLabel) { platformLabel.textContent = getTranslation('platformLabel', lang); } const nameLabel = document.querySelector('.name-label'); if (nameLabel) { nameLabel.textContent = getTranslation('nameLabel', lang); } const dateTimeLabel = document.querySelector('.date-time-label'); if (dateTimeLabel) { dateTimeLabel.textContent = getTranslation('dateTimeLabel', lang); } const goToLastElement = document.querySelector('.go-to-last-element'); if (goToLastElement) { goToLastElement.textContent = getTranslation('goToLastElement', lang); } const showStatsChart = document.querySelector('.show-stats-chart'); if (showStatsChart) { showStatsChart.textContent = getTranslation('showStatsChart', lang); } const deleteAll = document.querySelector('.delete-all'); if (deleteAll) { deleteAll.textContent = getTranslation('deleteAll', lang); } const exportButton = document.querySelector('.export-button'); if (exportButton) { exportButton.textContent = getTranslation('export', lang); } const importButton = document.querySelector('.import-button'); if (importButton) { importButton.textContent = getTranslation('import', lang); } const unblockAll = document.querySelector('.unblock-all'); if (unblockAll) { unblockAll.textContent = getTranslation('unblockAll', lang); } const blockAll = document.querySelector('.block-all'); if (blockAll) { blockAll.textContent = getTranslation('blockAll', lang); } const searchInput = document.querySelector('.search-input'); if (searchInput) { searchInput.placeholder = getTranslation('searchPlaceholder', lang); } const searchButton = document.querySelector('.search-button'); if (searchButton) { searchButton.textContent = getTranslation('searchButton', lang); } const twitchChannelLabel = document.querySelector('.twitch-channel-label'); if (twitchChannelLabel) { twitchChannelLabel.textContent = getTranslation('twitchChannelLabel', lang); } const addChannelInput = document.querySelector('.add-channel-input'); if (addChannelInput) { addChannelInput.placeholder = getTranslation('addChannelPlaceholder', lang); } const addChannelButton = document.querySelector('.add-channel-button'); if (addChannelButton) { addChannelButton.textContent = getTranslation('addChannelButton', lang); } settingsButton.title = getTranslation('settingsTitle', lang); } // Инициализация переводов панели при загрузке const scriptVersion = '2.6.52'; updatePanelTranslations(); // Обработчик клика по кнопке настроек settingsButton.onclick = () => { if (document.querySelector('.settings-popup')) { return; } GM_xmlhttpRequest({ method: 'GET', url: 'https://raw.githubusercontent.com/sopernik566/Control_Emotes_Panel_Twitch_JS/refs/heads/main/settingsPopup.html', onload: function(response) { try { const popup = document.createElement('div'); popup.innerHTML = response.responseText; // Убедимся, что класс .settings-popup добавлен только один раз const popupContainer = popup.querySelector('.settings-popup') || popup; popupContainer.className = 'settings-popup'; document.body.appendChild(popupContainer); // Запускаем анимацию открытия requestAnimationFrame(() => { popupContainer.classList.add('popup-enter'); }); const lang = GM_getValue('language', 'ru'); const closeButton = popupContainer.querySelector('.close-button'); if (closeButton) { closeButton.textContent = getTranslation('closeButton', lang); closeButton.onclick = () => { popupContainer.classList.remove('popup-enter'); popupContainer.classList.add('popup-exit'); popupContainer.addEventListener('animationend', () => { popupContainer.remove(); }, { once: true }); }; closeButton.onmouseover = () => { closeButton.style.background = ' #a88cc7'; }; closeButton.onmouseout = () => { closeButton.style.background = ' #2f213b'; }; } else { console.error('[7BTTVFZ Control Emotes Panel] Кнопка закрытия не найдена'); } const settingsTitle = popupContainer.querySelector('.settings-title'); if (settingsTitle) { settingsTitle.textContent = getTranslation('settingsTitle', lang); } const settingsDescription = popupContainer.querySelector('.settings-description'); if (settingsDescription) { settingsDescription.textContent = getTranslation('settingsDescription', lang); } const watchdogLabel = popupContainer.querySelector('.watchdog-label'); if (watchdogLabel) { watchdogLabel.textContent = getTranslation('watchdogLabel', lang); } const languageLabel = popupContainer.querySelector('.language-label'); if (languageLabel) { languageLabel.textContent = getTranslation('languageLabel', lang); } const watchdogInput = popupContainer.querySelector('.watchdog-interval'); if (watchdogInput) { watchdogInput.value = GM_getValue('watchdogInterval', 10); watchdogInput.onchange = () => { const interval = parseInt(watchdogInput.value, 10); if (interval >= 1) { GM_setValue('watchdogInterval', interval); console.log(`[7BTTVFZ Control Emotes Panel] Интервал watchdog установлен: ${interval} сек`); showNotification(getTranslation('watchdogUpdated', lang).replace('{interval}', interval), 3000); } else { showNotification(getTranslation('watchdogInvalid', lang), 3000); console.log('[7BTTVFZ Control Emotes Panel] Неверный интервал watchdog'); } }; } const languageSelect = popupContainer.querySelector('.language-select'); if (languageSelect) { languageSelect.value = GM_getValue('language', 'ru'); languageSelect.onchange = () => { const newLang = languageSelect.value; GM_setValue('language', newLang); console.log(`[7BTTVFZ Control Emotes Panel] Язык интерфейса изменён на: ${newLang}`); showNotification(getTranslation('languageUpdated', newLang), 3000); if (closeButton) closeButton.textContent = getTranslation('closeButton', newLang); if (settingsTitle) settingsTitle.textContent = getTranslation('settingsTitle', newLang); if (settingsDescription) settingsDescription.textContent = getTranslation('settingsDescription', newLang); if (watchdogLabel) watchdogLabel.textContent = getTranslation('watchdogLabel', newLang); if (languageLabel) languageLabel.textContent = getTranslation('languageLabel', newLang); updatePanelTranslations(newLang); }; } // Поддержка настройки размера шрифта (если активирована) const fontSizeInput = popupContainer.querySelector('.font-size-input'); if (fontSizeInput) { fontSizeInput.value = GM_getValue('buttonFontSize', 16); fontSizeInput.onchange = () => { const size = parseInt(fontSizeInput.value, 10); if (size >= 1 && size <= 35) { GM_setValue('buttonFontSize', size); console.log(`[7BTTVFZ Control Emotes Panel] Размер шрифта установлен: ${size}px`); showNotification(`Font size updated to ${size}px`, 3000); } else { showNotification('Font size must be between 1 and 35', 3000); console.log('[7BTTVFZ Control Emotes Panel] Неверный размер шрифта'); } }; } console.log('[7BTTVFZ Control Emotes Panel] Попап настроек загружен с GitHub'); } catch (err) { console.error('[7BTTVFZ Control Emotes Panel] Ошибка загрузки попапа:', err); showNotification(getTranslation('settingsFailed', lang), 3000); createFallbackSettingsPopup(); } }, onerror: function(err) { console.error('[7BTTVFZ Control Emotes Panel] Ошибка загрузки с GitHub:', err); showNotification(getTranslation('settingsFailed', GM_getValue('language', 'ru')), 3000); createFallbackSettingsPopup(); } }); }; // Резервный попап на случай сбоя function createFallbackSettingsPopup() { const settingsPopup = document.createElement('div'); settingsPopup.className = 'settings-popup'; const lang = GM_getValue('language', 'ru'); const closeButton = document.createElement('button'); closeButton.className = 'close-button'; closeButton.textContent = getTranslation('closeButton', lang); closeButton.onclick = () => { settingsPopup.classList.remove('popup-enter'); settingsPopup.classList.add('popup-exit'); settingsPopup.addEventListener('animationend', () => { settingsPopup.remove(); }, { once: true }); }; closeButton.onmouseover = () => { closeButton.style.background = ' #a88cc7'; }; closeButton.onmouseout = () => { closeButton.style.background = ' #2f213b'; }; settingsPopup.appendChild(closeButton); settingsPopup.innerHTML += ` <h3 class="settings-title">${getTranslation('settingsTitle', lang)}</h3> <div class="settings-content"> <p class="settings-description">${getTranslation('settingsFailed', lang)}</p> </div> `; document.body.appendChild(settingsPopup); // Запускаем анимацию открытия requestAnimationFrame(() => { settingsPopup.classList.add('popup-enter'); }); } // ================ конец end of popupsettings ==================== // //---------------Текст title "Название" "лист" список list of BlockedEmotes ------------------------// const title = document.createElement('h4'); title.innerText = 'list of BlockedEmotes'; title.style.margin = '-5px 0px 10px'; // Отступы сверху и снизу title.style.color = ' #2a1e38'; // Обновленный цвет title.style.position = 'relative'; // Устанавливаем позицию относительно title.style.bottom = '35px'; // Сдвиг по вертикали title.style.width = '190px'; controlPanel.appendChild(title); //--------------- Список заблокированных каналов ------------------// const list = document.createElement('ul'); list.id = 'blockedList'; list.style.position = 'relative'; list.style.bottom = '34px'; list.style.border = '1px solid #ffffff'; // Белая граница list.style.borderRadius = '0px 0px 8px 8px'; // Скругление углов list.style.boxShadow = ' rgb(0 0 0 / 67%) -18px 69px 40px 0 inset '; // Вставка тени в контейнер list.style.listStyle = 'none'; // Убираем стандартные маркеры списка list.style.padding = '0'; // Убираем отступы list.style.margin = '-14px 0px 10px'; // Отступ снизу list.style.maxHeight = '570px'; // Устанавливаем максимальную высоту list.style.height = '410px'; // Высота списка list.style.overflowY = 'auto'; // Включаем вертикальную прокрутку //==================================== ГРАДИЕНТ ФОН СПИСОК =================================================// // Добавляем линейный градиент фона с кроссбраузерностью 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'; // Белый цвет текста //========== кастомный scroll bar для списка =============// const style = document.createElement('style'); style.innerHTML = ` #blockedList::-webkit-scrollbar { width: 25px; /* Ширина скроллбара */ } #blockedList::-webkit-scrollbar-thumb { background-color: #C1A5EF; /* Цвет бегунка */ border-radius: 8px; /* Скругление бегунка */ border: 3px solid #4F3E6A; /* Внутренний отступ (цвет трека) */ height: 80px; /* Высота бегунка */ } #blockedList::-webkit-scrollbar-thumb:hover { background-color: #C6AEFF; /* Цвет бегунка при наведении */ } #blockedList::-webkit-scrollbar-thumb:active { background-color: #B097C9; /* Цвет бегунка при активном состоянии */ } #blockedList::-webkit-scrollbar-track { background: #455565; /* Цвет трека */ border-radius: 0px 0px 8px 0px; /* Скругление только нижнего правого угла */ } #blockedList::-webkit-scrollbar-track:hover { background-color: #455565; /* Цвет трека при наведении */ } #blockedList::-webkit-scrollbar-track:active { background-color: #455565; /* Цвет трека при активном состоянии */ } `; document.head.appendChild(style); // hover blocked-item элемент списка // const hoverStyle = document.createElement('style'); hoverStyle.innerHTML = ` .blocked-item { transition: background-color 0.3s ease, color 0.3s ease; } .blocked-item:hover { background-color: rgba(167, 54, 54, 0.52); color: #42d13a; } .blocked-item:hover span { color: #42d13a; } .new-item { background-color:#28a828; transition: background-color 0.3s ease; } .new-item:hover { background-color: #3a2252; color: #af7fcf; } .new-item:hover span { color: #af7fcf; } #sortContainer button { background: none; border: none; color: inherit; font-family: inherit; font-size: inherit; padding: 0 10px; transition: color 0.3s ease; } #sortContainer button:hover { color: #9ae048; } `; document.head.appendChild(hoverStyle); const highlightStyle = document.createElement('style'); highlightStyle.innerHTML = ` .blocked-item .highlight { background-color: #FFEB3B !important; /* Красный фон для подсветки */ color:rgb(0, 0, 0) !important; /* Белый текст для контраста */ padding: 0 2px !important; border-radius: 2px !important; transition: background-color 0.5s ease !important; } .blocked-item.highlight-item { background-color: rgba(163, 161, 18, 0.83) !important; /* Полупрозрачная красная подсветка для всего элемента */ transition: background-color 0.5s ease !important; } .last-item-highlight { background-color: #279479; /* Полупрозрачный золотой фон */ transition: background-color 0.5s ease; /* Плавное исчезновение */ } `; document.head.appendChild(highlightStyle); document.head.appendChild(style); const buttonColor = ' #907cad'; // Общий цвет для кнопок const buttonShadow = '0 4px 8px rgba(0, 0, 0, 0.6)'; // Тень для кнопок (60% прозрачности) // Функция для обновления списка заблокированных каналов // Переменные для хранения ID заблокированных элементов let blockedEmoteIDs = new Set(); let blockedChannelIDs = new Set(); let newlyAddedIds = new Set(); function updateBlockedList() { list.innerHTML = ''; // Очистка и обновление Set для быстрого поиска ID blockedEmoteIDs.clear(); blockedChannelIDs.clear(); function createListItem(channel, isNew = false) { const item = document.createElement('li'); item.className = 'blocked-item'; item.dataset.id = channel.id; if (isNew) { item.classList.add('new-item'); setTimeout(() => { item.classList.remove('new-item'); }, 1800000); } item.style.display = 'flex'; item.style.flexDirection = 'column'; item.style.padding = '5px'; item.style.borderBottom = '1px solid #eee'; const topRow = document.createElement('div'); topRow.style.display = 'flex'; topRow.style.justifyContent = 'space-between'; topRow.style.alignItems = 'center'; const channelName = document.createElement('span'); if (channel.platform === 'TwitchChannel') { channelName.innerText = `${channel.platform} > name emote: ${channel.emoteName}`; } else { channelName.innerText = `${channel.platform} > ${channel.emoteName}`; } channelName.classList.add('list-item-text'); channelName.style.flex = '1'; channelName.style.fontSize = '14px'; channelName.style.fontWeight = 'bold'; channelName.style.whiteSpace = 'nowrap'; channelName.style.overflow = 'hidden'; channelName.style.textOverflow = 'ellipsis'; topRow.appendChild(channelName); const dateInfo = document.createElement('span'); const date = new Date(channel.date); dateInfo.innerText = isNaN(date.getTime()) ? 'Unknown Date' : date.toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); dateInfo.classList.add('list-item-date'); dateInfo.style.marginRight = '30px'; dateInfo.style.fontSize = '14px'; topRow.appendChild(dateInfo); const removeButton = document.createElement('button'); removeButton.innerText = 'Delete'; removeButton.classList.add('delete-button'); console.log("[7BTTVFZ Control Emotes Panel] Создана кнопка Delete для элемента:", channel.id); // Используем сохранённые стили из currentDeleteButtonStyles removeButton.style.background = currentDeleteButtonStyles.background; removeButton.style.color = currentDeleteButtonStyles.color; removeButton.style.height = '35px'; removeButton.style.width = '75px'; removeButton.style.fontWeight = 'bold'; removeButton.style.fontSize = '16px'; removeButton.style.border = 'none'; removeButton.style.borderRadius = '4px'; removeButton.style.cursor = 'pointer'; removeButton.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.6)'; removeButton.style.display = 'flex'; removeButton.style.alignItems = 'center'; removeButton.style.justifyContent = 'center'; removeButton.style.transition = 'background 0.3s ease'; removeButton.style.opacity = '1'; removeButton.style.visibility = 'visible'; // Добавляем обработчики наведения removeButton.onmouseover = () => { removeButton.style.background = currentDeleteButtonStyles.hoverBackground; }; removeButton.onmouseout = () => { removeButton.style.background = currentDeleteButtonStyles.background; }; removeButton.onclick = function () { if (channel.platform === 'TwitchChannel') { blockedChannels = blockedChannels.filter(c => c.id !== channel.id); blockedChannelIDs.delete(channel.id); GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2)); } else { blockedEmotes = blockedEmotes.filter(c => c.id !== channel.id); blockedEmoteIDs.delete(channel.id); GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2)); } newlyAddedIds.delete(channel.id); updateBlockedList(); updateCounter(); showEmoteForChannel(channel); }; topRow.appendChild(removeButton); item.appendChild(topRow); const channelLink = document.createElement('span'); channelLink.innerText = `(prefix: ${channel.name})`; channelLink.classList.add('list-item-link'); channelLink.style.fontSize = '14px'; channelLink.style.wordBreak = 'break-word'; channelLink.style.marginTop = '1px'; item.appendChild(channelLink); return item; } // Заполняем списки и обновляем Set blockedChannels.forEach(channel => { blockedChannelIDs.add(channel.id); const isNew = newlyAddedIds.has(channel.id) && Array.from(newlyAddedIds).pop() === channel.id; list.appendChild(createListItem(channel, isNew)); }); blockedEmotes.forEach(channel => { blockedEmoteIDs.add(channel.id); const isNew = newlyAddedIds.has(channel.id) && Array.from(newlyAddedIds).pop() === channel.id; list.appendChild(createListItem(channel, isNew)); }); // Прокручиваем к последнему добавленному элементу внутри контейнера list if (newlyAddedIds.size > 0) { const lastAddedId = Array.from(newlyAddedIds).pop(); const newItem = list.querySelector(`[data-id="${lastAddedId}"]`); if (newItem) { // Вычисляем позицию нового элемента относительно контейнера list const itemOffsetTop = newItem.offsetTop; // Позиция элемента относительно начала списка const listHeight = list.clientHeight; // Видимая высота контейнера list const itemHeight = newItem.clientHeight; // Высота самого элемента // Вычисляем, куда нужно прокрутить, чтобы элемент оказался вверху видимой области const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2); // Плавно прокручиваем list к нужной позиции list.scrollTo({ top: scrollPosition, behavior: 'smooth' }); } } // Очищаем список новых ID после отображения newlyAddedIds.clear(); } // Добавляем список в панель управления controlPanel.appendChild(list); // Создаём контейнер для поисковой строки const searchContainer = document.createElement('div'); searchContainer.style.display = 'flex'; searchContainer.style.gap = '5px'; searchContainer.style.top = '500px'; searchContainer.style.position = 'relative'; // Создаём поисковую строку const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = 'Search in blocked list...'; searchInput.style.background = ' #192427'; searchInput.style.width = '459px'; searchInput.style.left = '132px'; searchInput.style.color = ' #b69dcf'; searchInput.style.fontWeight = 'bold'; searchInput.style.height = '35px'; searchInput.style.padding = '5px'; searchInput.style.border = '1px solid #b69dcf'; searchInput.style.borderRadius = '4px'; searchInput.style.boxShadow = ' #4c2a5e 0px 4px 6px inset'; searchInput.style.position = 'relative'; searchInput.style.bottom = '50px'; // Создаём кнопку поиска const searchButton = document.createElement('button'); searchButton.innerText = 'Search'; // Меняем текст на "Search" searchButton.style.background = buttonColor; searchButton.style.position = 'relative'; searchButton.style.bottom = '50px'; searchButton.style.color = ' #fff'; searchButton.style.border = 'none'; searchButton.style.width = '72px'; searchButton.style.left = '132px'; searchButton.style.borderRadius = '4px'; searchButton.style.padding = '5px 10px'; searchButton.style.cursor = 'pointer'; searchButton.style.fontSize = '16px'; searchButton.style.fontWeight = 'bold'; searchButton.style.boxShadow = buttonShadow; // Добавляем ховер-эффекты для кнопки поиска searchButton.onmouseover = function() { searchButton.style.background = '-webkit-linear-gradient(135deg, #443157 0%,rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)'; }; searchButton.onmouseout = function() { searchButton.style.background = buttonColor; }; // Обработчик кнопки поиска searchButton.onclick = () => { const searchTerm = searchInput.value.trim(); filterBlockedList(searchTerm); // Запускаем фильтрацию }; function filterBlockedList(searchTerm) { const lowerSearchTerm = searchTerm.toLowerCase().trim(); console.log("[7BTTVFZ Control Emotes Panel] Поисковый запрос (lowerSearchTerm):", lowerSearchTerm); let filteredList = []; // Фильтрация списка if (!lowerSearchTerm) { filteredList = [...blockedChannels, ...blockedEmotes]; console.log("[7BTTVFZ Control Emotes Panel] Поиск пустой, отображаем все элементы:", filteredList); } else { filteredList = [...blockedChannels, ...blockedEmotes].filter(item => { const emoteName = item.emoteName || ''; const platform = item.platform || ''; const name = item.name || ''; const matches = emoteName.toLowerCase().includes(lowerSearchTerm) || platform.toLowerCase().includes(lowerSearchTerm) || name.toLowerCase().includes(lowerSearchTerm); console.log(`[7BTTVFZ Control Emotes Panel] Проверяем элемент: ${JSON.stringify(item)}, совпадение: ${matches}`); return matches; }); console.log("[7BTTVFZ Control Emotes Panel] Результаты фильтрации:", filteredList); } // Сохраняем текущую позицию скролла const currentScrollPosition = list.scrollTop; console.log("[7BTTVFZ Control Emotes Panel] Текущая позиция скролла перед обновлением:", currentScrollPosition); // Получаем текущие элементы в DOM const currentItems = list.querySelectorAll('.blocked-item'); const existingIds = new Set([...currentItems].map(item => item.dataset.id)); // Удаляем элементы, которые не прошли фильтрацию currentItems.forEach(item => { const itemId = item.dataset.id; if (!filteredList.some(f => f.id === itemId)) { item.remove(); } }); // Добавляем или обновляем элементы filteredList.forEach(channel => { const itemId = channel.id; let item = list.querySelector(`[data-id="${itemId}"]`); if (!item) { // Если элемента нет, создаём новый item = createListItem(channel); list.appendChild(item); } // Применяем подсветку, если есть поисковый запрос if (lowerSearchTerm) { const spans = item.querySelectorAll('span'); spans.forEach(span => { const originalText = span.textContent || ''; if (originalText.toLowerCase().includes(lowerSearchTerm)) { const regex = new RegExp(`(${lowerSearchTerm})`, 'gi'); const highlightedText = originalText.replace(regex, '<span class="highlight">$1</span>'); span.innerHTML = highlightedText; } }); } }); // Прокрутка к первому элементу if (filteredList.length > 0) { const firstItem = list.querySelector('.blocked-item'); if (firstItem) { console.log("[7BTTVFZ Control Emotes Panel] Найден первый элемент для прокрутки:", firstItem); const firstItemOffset = firstItem.offsetTop; list.scrollTop = firstItemOffset - (list.clientHeight / 2) + (firstItem.clientHeight / 2); console.log("[7BTTVFZ Control Emotes Panel] Установлен scrollTop:", list.scrollTop); } else { console.log("[7BTTVFZ Control Emotes Panel] Первый элемент не найден в DOM!"); } } else { // Если список пуст, восстанавливаем скролл console.log("[7BTTVFZ Control Emotes Panel] Список пуст, восстанавливаем скролл на:", currentScrollPosition); list.scrollTop = currentScrollPosition; } updateCounter(); } // Добавляем элементы в контейнер поиска searchContainer.appendChild(searchInput); searchContainer.appendChild(searchButton); // searchButton // Добавляем контейнер поиска в панель управления controlPanel.appendChild(searchContainer); // Далее продолжаем с добавлением списка 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 to add channel '; input.style.position = 'relative'; input.style.background = ' #192427'; input.style.color = ' #b69dcf'; input.style.flex = '1'; input.style.fontWeight = 'bold'; // Жирный текст input.style.height = '35px'; // Отступ между кнопкой и поисковой строкой input.style.padding = '5px'; input.style.border = '1px solid #b69dcf'; input.style.borderRadius = '4px'; input.style.top = '15px'; // Отступ между кнопкой и поисковой строкой // Добавление тени с фиолетовым цветом (35% прозрачности) внутрь input.style.boxShadow = ' #4c2a5e 0px 4px 6px inset'; // Тень фиолетового цвета внутри //================== Add it Button =====================// // ==================== Кнопка добавления ===================== // const addButton = document.createElement('button'); addButton.innerText = 'Add it'; addButton.style.background = buttonColor; addButton.style.top = '15px'; // Отступ между кнопкой и поисковой строкой addButton.style.position = 'relative'; addButton.style.color = ' #fff'; addButton.style.border = 'none'; addButton.style.width = '72px'; addButton.style.borderRadius = '4px'; addButton.style.padding = '5px 10px'; addButton.style.cursor = 'pointer'; addButton.style.boxShadow = buttonShadow; // Тень для кнопки "Add it" // Увеличиваем размер текста и делаем его жирным addButton.style.fontSize = '16px'; // Увеличиваем размер текста addButton.style.fontWeight = 'bold'; // Жирный текст // Генерация уникального ID function generateID() { return `emote_${Date.now()}`; // Генерация ID на основе времени } addButton.onclick = (event) => { event.preventDefault(); const channel = input.value.trim(); const platform = platformSelect.value; if (channel) { let emoteName = channel; let emoteUrl = channel; const emoteId = generateRandomID(); // Проверка на дублирование const isDuplicate = platform === 'TwitchChannel' ? blockedChannels.some(e => e.name === channel && e.platform === platform) : blockedEmotes.some(e => e.emoteUrl === channel && e.platform === platform); if (isDuplicate) { console.log(`%c[7BTTVFZ Control Emotes Panel] %cChannel/Emote already blocked: ${channel}`, 'color: rgb(255, 165, 0); font-weight: bold;', 'color: rgb(255, 165, 0);'); return; } if (platform === '7tv' || platform === 'bttTV' || platform === 'ffz') { const img = document.querySelector(`img[src="${channel}"]`); if (img) { emoteName = img.alt || channel.split('/').pop(); emoteUrl = img.src || channel; } const newEmote = { id: emoteId, name: emoteUrl, platform: platform, emoteName: emoteName, emoteUrl: emoteUrl, date: new Date().toISOString() }; blockedEmotes.push(newEmote); blockedEmoteIDs.add(emoteId); newlyAddedIds.add(emoteId); GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2)); console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedEmotes:`, 'color: rgb(0, 255, 0); font-weight: bold;', 'color: rgb(0, 255, 0);', newEmote); } else if (platform === 'TwitchChannel') { const prefix = channel.split(/[^a-zA-Z0-9]/)[0]; emoteUrl = prefix; const newChannel = { id: emoteId, name: emoteUrl, platform: platform, emoteName: emoteName, emoteUrl: emoteUrl, date: new Date().toISOString() }; blockedChannels.push(newChannel); blockedChannelIDs.add(emoteId); newlyAddedIds.add(emoteId); GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2)); console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedChannels:`, 'color: rgb(0, 255, 0); font-weight: bold;', 'color: rgb(0, 255, 0);', newChannel); } const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { toggleEmotesInNode(chatContainer); } updateBlockedList(); updateCounter(); input.value = ''; } }; // ==================== Создание выпадающего списка платформ ===================== // const platformSelect = document.createElement('select'); platformSelect.style.top = '15px'; // Отступ между кнопкой и поисковой строкой platformSelect.style.position = 'relative'; platformSelect.style.height = '35px'; // Высота выпадающего списка platformSelect.style.border = '1px solid #c1a5ef'; platformSelect.style.background = ' #192427'; platformSelect.style.borderRadius = '4px'; platformSelect.style.padding = '5px'; platformSelect.style.fontWeight = 'bold'; // Жирный текст platformSelect.style.color = ' #b69dcf'; const platforms = ['TwitchChannel', '7tv', 'bttTV', 'ffz']; platforms.forEach(platform => { const option = document.createElement('option'); option.value = platform; option.innerText = platform; platformSelect.appendChild(option); }); // ==================== Подсказки для выбора платформы ===================== // platformSelect.addEventListener('change', () => { const placeholderText = { 'TwitchChannel': 'example prefix abcd123', '7tv': 'link example: https://cdn.7tv.app/emote/00000000000000000000000000/2x.webp', 'bttTV': 'link example: https://cdn.betterttv.net/emote/000000000000000000000000/2x.webp', 'ffz': 'link example: https://cdn.frankerfacez.com/emote/0000/2' }; input.placeholder = placeholderText[platformSelect.value]; }); // ==================== Добавление выпадающего списка в контейнер ===================== // inputContainer.appendChild(platformSelect); //----------------Единый контейнер для кнопок -------------------------// const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; // Используем flexbox для расположения кнопок в строку buttonContainer.style.alignItems = 'baseline'; // Используем flexbox для расположения кнопок в строку buttonContainer.style.alignContent = 'stretch'; buttonContainer.style.gap = '13px'; // Задаем промежуток между кнопками buttonContainer.style.bottom = '113px'; // Отступ сверху для контейнера кнопок buttonContainer.style.position = 'relative'; // Позиционирование относительно buttonContainer.style.fontWeight = 'bold'; // Жирный текст для контейнера кнопок buttonContainer.style.fontSize = '16px'; // Размер шрифта для кнопок buttonContainer.style.width = '668px'; // Ширина кнопок (увеличена для эффекта растяжения //-------------- Кнопка "Delete all" ------------------------// const clearAllButton = document.createElement('button'); clearAllButton.innerText = 'Delete all'; // Текст на кнопке clearAllButton.style.background = buttonColor; // Цвет фона кнопки clearAllButton.style.color = ' #fff'; // Цвет текста кнопки clearAllButton.style.border = 'none'; // Убираем бордер у кнопки clearAllButton.style.borderRadius = '4px'; // Скругленные углы кнопки clearAllButton.style.padding = '5px 10px'; // Отступы внутри кнопки clearAllButton.style.cursor = 'pointer'; // Курсор в виде руки при наведении clearAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Delete all" buttonContainer.appendChild(clearAllButton); // Добавляем кнопку в контейнер // Обработчик события для кнопки "Delete all" clearAllButton.onclick = () => { blockedEmotes = []; blockedChannels = []; GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2)); GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2)); console.log("[7BTTVFZ Control Emotes Panel] Очищены blockedEmotes и blockedChannels"); updateBlockedList(); updateCounter(); }; //----------------- export Button --------------------// const exportButton = document.createElement('button'); exportButton.innerText = 'Export'; exportButton.style.background = buttonColor; exportButton.style.color = ' #fff'; exportButton.style.border = 'none'; exportButton.style.borderRadius = '4px'; exportButton.style.padding = '5px 10px'; exportButton.style.cursor = 'pointer'; exportButton.style.boxShadow = buttonShadow; // Тень для кнопки "Export" buttonContainer.appendChild(exportButton); exportButton.onclick = () => { const combinedData = { blockedEmotes: blockedEmotes, blockedChannels: blockedChannels }; const blob = new Blob([JSON.stringify(combinedData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'blocked_data.json'; link.click(); URL.revokeObjectURL(url); console.log("[7BTTVFZ Control Emotes Panel] Экспортированы данные:", combinedData); }; //================= importButton ========================// // Перемещаем создание fileInput в глобальную область, чтобы избежать дублирования let fileInput = null; // Функция для создания кнопки "Import" function createImportButton() { const button = document.createElement('button'); button.innerText = 'Import'; button.style.background = buttonColor; button.style.color = ' #fff'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.padding = '5px 10px'; button.style.cursor = 'pointer'; button.style.boxShadow = buttonShadow; return button; } // Функция для создания или переиспользования элемента input типа "file" function createFileInput() { if (!fileInput) { fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.style.display = 'none'; fileInput.onchange = handleFileChange; document.body.appendChild(fileInput); } return fileInput; } // Инициализация кнопки "Import" const importButton = createImportButton(); buttonContainer.appendChild(importButton); importButton.onclick = () => { const input = createFileInput(); input.value = ''; // Сбрасываем значение для повторного выбора файла input.click(); }; // Обработка изменений файла function handleFileChange(event) { const file = event.target.files[0]; if (!file) { console.log("[7BTTVFZ Control Emotes Panel] Файл не выбран"); return; } const reader = new FileReader(); reader.onload = handleFileLoad; reader.onerror = () => { console.error("[7BTTVFZ Control Emotes Panel] Ошибка чтения файла"); alert('Ошибка при чтении файла!'); }; reader.readAsText(file); } // Обработка загрузки файла function handleFileLoad(event) { try { const importedData = JSON.parse(event.target.result); if (!importedData || (!importedData.blockedEmotes && !importedData.blockedChannels)) { throw new Error('Неверный формат файла! Ожидается объект с blockedEmotes и/или blockedChannels'); } processImportedData(importedData); updateInterface(); console.log("[7BTTVFZ Control Emotes Panel] Импорт успешно завершен"); } catch (err) { console.error('[7BTTVFZ Control Emotes Panel] Ошибка при парсинге файла:', err); alert(`Ошибка импорта: ${err.message}`); } } // Обработка импортированных данных function processImportedData(importedData) { blockedEmotes = []; blockedChannels = []; blockedEmoteIDs.clear(); blockedChannelIDs.clear(); newlyAddedIds.clear(); if (Array.isArray(importedData.blockedEmotes)) { importedData.blockedEmotes.forEach(emote => { const newId = emote.id && !blockedEmoteIDs.has(emote.id) && !blockedChannelIDs.has(emote.id) ? emote.id : generateRandomID(); const newEmote = { id: newId, name: emote.name || emote.emoteUrl || '', platform: emote.platform || 'unknown', emoteName: emote.emoteName || getDefaultEmoteName(emote), emoteUrl: emote.emoteUrl || emote.name || '', date: emote.date || new Date().toISOString() }; blockedEmotes.push(newEmote); blockedEmoteIDs.add(newId); newlyAddedIds.add(newId); }); } if (Array.isArray(importedData.blockedChannels)) { importedData.blockedChannels.forEach(channel => { const newId = channel.id && !blockedChannelIDs.has(channel.id) && !blockedEmoteIDs.has(channel.id) ? channel.id : generateRandomID(); const newChannel = { id: newId, name: channel.name || channel.emoteUrl || '', platform: channel.platform || 'TwitchChannel', emoteName: channel.emoteName || getDefaultEmoteName(channel), emoteUrl: channel.emoteUrl || channel.name || '', date: channel.date || new Date().toISOString() }; blockedChannels.push(newChannel); blockedChannelIDs.add(newId); newlyAddedIds.add(newId); }); } GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2)); GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2)); console.log("[7BTTVFZ Control Emotes Panel] Импортированы blockedEmotes:", blockedEmotes); console.log("[7BTTVFZ Control Emotes Panel] Импортированы blockedChannels:", blockedChannels); } // Функция обновления интерфейса function updateInterface() { blockedEmotes = loadData("blockedEmotes", []); blockedChannels = loadData("blockedChannels", []); blockedEmoteIDs.clear(); blockedChannelIDs.clear(); blockedEmotes.forEach(emote => blockedEmoteIDs.add(emote.id)); blockedChannels.forEach(channel => blockedChannelIDs.add(channel.id)); updateBlockedList(); updateCounter(); const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { toggleEmotesInNode(chatContainer); // Используем toggleEmotesInNode вместо hideEmotesForChannel } else { console.log( "%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден при обновлении интерфейса", 'color:rgb(218, 93, 9); font-weight: bold;', 'color: rgb(218, 93, 9);' ); } } // Функция скрытия эмодзи в чате function hideEmotesForChannel(chatContainer) { console.log("[7BTTVFZ Control Emotes Panel] Запуск hideEmotesForChannel"); const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote'); emotes.forEach(emote => { const emoteUrl = emote.src || ''; const emoteAlt = emote.getAttribute('alt') || ''; let blockedEntry = null; // Проверяем, заблокирован ли эмодзи if (emoteUrl.includes('7tv.app')) { blockedEntry = blockedEmotes.find(e => e.platform === '7tv' && e.emoteUrl === emoteUrl); } else if (emoteUrl.includes('betterttv.net')) { blockedEntry = blockedEmotes.find(e => e.platform === 'bttTV' && e.emoteUrl === emoteUrl); } else if (emoteAlt) { blockedEntry = blockedChannels.find(e => e.platform === 'TwitchChannel' && emoteAlt.startsWith(e.name)); } // Устанавливаем data-emote-id, если эмодзи заблокирован if (blockedEntry && !emote.getAttribute('data-emote-id')) { emote.setAttribute('data-emote-id', blockedEntry.id); } const emoteId = emote.getAttribute('data-emote-id'); const isBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId)); // Скрываем или показываем эмодзи emote.style.display = isBlocked ? 'none' : ''; console.log(`[7BTTVFZ Control Emotes Panel] Эмодзи ${emoteAlt || emoteUrl} (ID: ${emoteId || 'не установлен'}) ${isBlocked ? 'скрыт' : 'показан'}`); }); } // Функция получения имени эмотикона по умолчанию function getDefaultEmoteName(channel) { if (channel.platform === '7tv' || channel.platform === 'bttTV') { return channel.name.split('/').slice(-2, -1)[0] || 'No Name'; } else if (channel.platform === 'ffz') { return channel.emoteName || channel.name.split('/').pop() || 'No Name'; } else if (channel.platform === 'TwitchChannel') { return channel.name.split(/[^a-zA-Z0-9]/)[0] || 'No Name'; } else { return 'No Name'; } } // Добавляем кнопку "Unblock All Emotes" в контейнер кнопок const unblockAllButton = document.createElement('button'); unblockAllButton.innerText = 'Unblock All Emotes'; unblockAllButton.style.background = buttonColor; unblockAllButton.style.color = ' #fff'; unblockAllButton.style.border = 'none'; unblockAllButton.style.borderRadius = '4px'; unblockAllButton.style.padding = '5px 10px'; unblockAllButton.style.cursor = 'pointer'; unblockAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Unblock All Emotes" buttonContainer.appendChild(unblockAllButton); // Добавляем кнопку "Back To Block All Emotes" в контейнер кнопок const blockAllButton = document.createElement('button'); blockAllButton.innerText = 'Back To Block All Emotes'; blockAllButton.style.background = buttonColor; blockAllButton.style.color = ' #fff'; blockAllButton.style.border = 'none'; blockAllButton.style.borderRadius = '4px'; blockAllButton.style.padding = '5px 10px'; blockAllButton.style.cursor = 'pointer'; blockAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Back To Block All Emotes" buttonContainer.appendChild(blockAllButton); // Обработчик события для кнопки "Unblock All Emotes" unblockAllButton.onclick = () => { const unblockedEmotes = GM_getValue('unblockedEmotes', []); const unblockedChannels = GM_getValue('unblockedChannels', []); if (blockedEmotes.length > 0 || blockedChannels.length > 0) { GM_setValue('unblockedEmotes', blockedEmotes); GM_setValue('unblockedChannels', blockedChannels); blockedEmotes = []; blockedChannels = []; GM_setValue('blockedEmotes', JSON.stringify(blockedEmotes, null, 2)); // Исправлено GM_setValue('blockedChannels', JSON.stringify(blockedChannels, null, 2)); // Исправлено console.log("[7BTTVFZ Control Emotes Panel] Разблокированы все: unblockedEmotes:", blockedEmotes, "unblockedChannels:", blockedChannels); updateBlockedList(); updateCounter(); showAllEmotes(); } }; // Функция для отображения всех смайлов в чате function showAllEmotes() { const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote'); emotes.forEach(emote => { emote.style.display = ''; // Сбросить стиль display для отображения смайлов }); } } // Обработчик события для кнопки "Back To Block All Emotes" blockAllButton.onclick = () => { const unblockedEmotes = GM_getValue('unblockedEmotes', []); const unblockedChannels = GM_getValue('unblockedChannels', []); if (unblockedEmotes.length > 0 || unblockedChannels.length > 0) { blockedEmotes = unblockedEmotes; blockedChannels = unblockedChannels; GM_setValue('blockedEmotes', JSON.stringify(blockedEmotes)); GM_setValue('blockedChannels', JSON.stringify(blockedChannels)); GM_setValue('unblockedEmotes', []); GM_setValue('unblockedChannels', []); console.log("[7BTTVFZ Control Emotes Panel] Заблокированы все обратно: blockedEmotes:", blockedEmotes, "blockedChannels:", blockedChannels); // Обновляем список и счетчик updateBlockedList(); updateCounter(); // Применяем скрытие эмодзи в чате const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { toggleEmotesInNode(chatContainer); console.log("[7BTTVFZ Control Emotes Panel] Применено скрытие эмодзи после восстановления блокировки"); } else { console.log( "%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден при восстановлении блокировки", 'color:rgb(218, 93, 9); font-weight: bold;', 'color: rgb(218, 93, 9);' ); } } }; // Кнопка "Show Stats Chart" const showStatsButton = document.createElement('button'); showStatsButton.innerText = 'Show Stats Chart'; showStatsButton.style.cursor = 'pointer'; showStatsButton.style.position = 'relative'; showStatsButton.style.right = '1%'; // Создаём модальное окно для диаграммы const chartModal = document.createElement('div'); chartModal.style.position = 'fixed'; chartModal.style.top = '35%'; chartModal.style.left = '35%'; chartModal.style.width = '35%'; chartModal.style.height = '55%'; chartModal.style.display = 'none'; // Скрыто по умолчанию chartModal.style.justifyContent = 'center'; chartModal.style.alignItems = 'center'; chartModal.style.zIndex = '10001'; // Создаём контейнер для диаграммы const chartContainer = document.createElement('div'); chartContainer.style.background = 'linear-gradient(315deg, hsla(285, 61%, 12%, 1) 0%, hsla(186, 26%, 21%, 1) 55%, hsla(284, 9%, 48%, 1) 100%)';chartContainer.style.padding = '20px'; chartContainer.style.borderRadius = '20px'; chartContainer.style.boxShadow = '16px 20px 20px 5px #0000008c'; chartContainer.style.border = '2px solid #24888e'; chartContainer.style.position = 'relative'; chartContainer.style.width = '600px'; chartContainer.style.maxHeight = '80vh'; chartContainer.style.overflowY = 'auto'; // Создаём кнопку закрытия модального окна const closeChartButton = document.createElement('button'); closeChartButton.innerText = 'Close'; closeChartButton.style.position = 'absolute'; closeChartButton.style.top = '10px'; closeChartButton.style.right = '10px'; closeChartButton.style.background = ' #944646'; closeChartButton.style.color = ' #fff'; closeChartButton.style.border = 'none'; closeChartButton.style.borderRadius = '4px'; closeChartButton.style.padding = '5px 10px'; closeChartButton.style.cursor = 'pointer'; closeChartButton.onclick = () => { chartModal.style.display = 'none'; // Уничтожаем диаграмму, чтобы избежать утечек памяти const existingChart = Chart.getChart('statsChart'); if (existingChart) { existingChart.destroy(); } }; // Создаём элемент canvas для диаграммы const chartCanvas = document.createElement('canvas'); chartCanvas.id = 'statsChart'; chartCanvas.style.maxWidth = '100%'; chartCanvas.style.height = '400px'; // Добавляем элементы в модальное окно chartContainer.appendChild(closeChartButton); chartContainer.appendChild(chartCanvas); chartModal.appendChild(chartContainer); document.body.appendChild(chartModal); // Обработчик клика на кнопку "Show Stats Chart" showStatsButton.onclick = () => { // Показываем модальное окно chartModal.style.display = 'flex'; // Собираем данные для диаграммы const twitchCount = blockedChannels.length; const bttvCount = blockedEmotes.filter(channel => channel.platform === 'bttTV').length; const tv7Count = blockedEmotes.filter(channel => channel.platform === '7tv').length; const ffzCount = blockedEmotes.filter(channel => channel.platform === 'ffz').length; // Данные для диаграммы const chartData = { labels: ['Twitch', 'BTTV', '7TV', 'FFZ'], datasets: [{ label: 'Blocked Emotes by Platform', data: [twitchCount, bttvCount, tv7Count, ffzCount], backgroundColor: [ 'rgba(96, 37, 136, 0.6)', // Twitch 'rgba(214, 95, 91, 0.6)', // BTTV 'rgba(34, 196, 196, 0.6)', // 7TV 'rgba(121, 117, 117, 0.66)' // FFZ ], borderColor: [ 'rgb(130, 255, 99)', 'rgb(97, 183, 240)', 'rgb(255, 238, 86)', 'rgb(74, 221, 221)' ], borderWidth: 1 }] }; // Уничтожаем старую диаграмму, если она существует const existingChart = Chart.getChart('statsChart'); if (existingChart) { existingChart.destroy(); } // Создаём новую диаграмму new Chart(chartCanvas, { type: 'bar', data: chartData, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { color: ' #E5E7EB' // Цвет текста легенды } }, title: { display: true, text: 'Blocked Emotes by Platform', color: ' #E5E7EB' // Цвет текста заголовка } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Number of Blocked Emotes', color: ' #E5E7EB' // Цвет текста заголовка оси Y }, ticks: { color: ' #E5E7EB' // Цвет текста значений на оси Y } }, x: { title: { display: true, text: 'Platform', color: ' #E5E7EB' // Цвет текста заголовка оси X }, ticks: { color: ' #E5E7EB' // Цвет текста значений на оси X } } } } }); }; //======================= Счётчик ========================// const counter = document.createElement('div'); counter.style.display = 'flex'; counter.style.flexDirection = 'row'; counter.style.justifyContent = 'center'; counter.style.width = '460px'; counter.style.backgroundColor = ' #b69dcf'; // Белый фон counter.style.color = ' #4c2a5e'; // Цвет текста (темно-фиолетовый) counter.style.border = '3px solid #4c2a5e'; // Граница того же цвета, что и текст counter.style.borderRadius = '8px'; // Радиус скругления границы counter.style.padding = '5px 0px'; // Отступы для удобства counter.style.marginLeft = '6px'; // Отступ слева для отделения от других элементов counter.style.fontWeight = 'bold'; // Жирное начертание текста counter.style.fontSize = '16px'; // Устанавливаем размер шрифта для лучшей видимости counter.style.bottom = '545px'; // Обновленное положение сверху counter.style.left = '202px'; // Обновленное положение справа counter.style.position = 'relative '; // Относительное позиционирование для точного расположения controlPanel.appendChild(counter); // Функция для обновления счётчика function updateCounter() { const twitchCount = blockedChannels.length; const bttvCount = blockedEmotes.filter(channel => channel.platform === 'bttTV').length; const tv7Count = blockedEmotes.filter(channel => channel.platform === '7tv').length; const ffzCount = blockedEmotes.filter(channel => channel.platform === 'ffz').length; const totalCount = twitchCount + bttvCount + tv7Count + ffzCount; counter.innerText = `Twitch: ${twitchCount} | BTTV: ${bttvCount} | 7TV: ${tv7Count} | FFZ: ${ffzCount} | Total: ${totalCount}`; } // Добавляем элементы на страницу inputContainer.appendChild(input); inputContainer.appendChild(addButton); controlPanel.appendChild(inputContainer); // Перемещаем контейнер кнопок вниз controlPanel.appendChild(buttonContainer); document.body.appendChild(controlPanel); // Вызываем функцию обновления счётчика updateCounter(); //============= Создаем кнопку "Open Panel " ===================// // Загружаем сохранённое состояние переключателя из хранилища const openPanelButton = document.createElement('button'); openPanelButton.innerText = 'panel control emotes'; openPanelButton.style.fontWeight = 'bold'; openPanelButton.style.top = '22px'; openPanelButton.style.right = '1344px'; openPanelButton.style.position = 'fixed'; // Фиксированное положение openPanelButton.style.width = '200px'; // Фиксированная ширина кнопки openPanelButton.style.height = '41px'; // Фиксированная высота кнопки openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки openPanelButton.style.color = ' #171c1c'; 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 = ' #171c1c'; // Темно сеая рамка для кружка 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 = ' #5d5d5d'; // темно Серый цвет кружка 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); // Функция для обновления состояния переключателя const updateSwitchState = () => { if (isPanelOpen) { openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки при открытой панели switchCircle.style.transform = 'translateX(20px)'; // Перемещаем кружок вправо switchContainer.style.backgroundColor = ' #464646'; // Цвет контейнера в включённом состоянии controlPanel.style.display = 'block'; // Показываем панель controlPanel.style.height = '656px'; // Устанавливаем полную высоту } else { openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки при закрытой панели switchCircle.style.transform = 'translateX(0)'; // Перемещаем кружок влево switchContainer.style.backgroundColor = ' #171c1c'; // Цвет контейнера в выключенном состоянии controlPanel.style.display = 'none'; // Скрываем панель controlPanel.style.height = '0px'; // Сворачиваем панель } }; // Обработчик клика для переключения состояния панели openPanelButton.onclick = () => { isPanelOpen = !isPanelOpen; // Переключаем состояние GM_setValue('isPanelOpen', isPanelOpen); // Сохраняем состояние updateSwitchState(); // Обновляем видимость и переключатель }; // Инициализация состояния при загрузке window.addEventListener('load', () => { document.body.appendChild(openPanelButton); updateSwitchState(); // Устанавливаем начальное состояние панели и переключателя const updateButtonPosition = () => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; openPanelButton.style.top = `${windowHeight * 0.005}px`; // 5% от высоты окна openPanelButton.style.right = `400px`; // Фиксированный отступ 50px от правого }; updateButtonPosition(); window.addEventListener('resize', updateButtonPosition); }); // ========================== end of open panelButton ================================= // // =================== Вставки в контейнер "penPanelButton" рядом с кнопкой "Go Ad-Free for Free" =================== // // Функция для Вставки в контейнер "penPanelButton" рядом с кнопкой "Go Ad-Free for Free" function addOpenPanelButtonToNav() { // Находим контейнер кнопки "Notifications" const notificationsButtonContainer = document.querySelector('button[aria-label="Open Notifications"]')?.closest('.Layout-sc-1xcs6mc-0.czRfnU'); if (!notificationsButtonContainer) { console.error('[7BTTVFZ Control Emotes Panel] Контейнер для кнопки "Notifications" не найден'); return; } // Проверяем, не добавлена ли кнопка уже if (notificationsButtonContainer.querySelector('#openPanelButton')) { console.log('[7BTTVFZ Control Emotes Panel] Кнопка openPanelButton уже добавлена'); return; } // Создаём контейнер для openPanelButton, чтобы соответствовать стилю Twitch const buttonWrapper = document.createElement('div'); buttonWrapper.className = 'Layout-sc-1xcs6mc-0 cwtKyw tw-mg-x-05'; // Добавляем tw-mg-x-05 для отступов buttonWrapper.style.marginLeft = '10px'; // Отступ слева от кнопки Notifications buttonWrapper.style.marginRight = '5px'; // Отступ справа для следующего элемента // Создаём внутренний контейнер для стилизации const innerWrapper = document.createElement('div'); innerWrapper.className = 'InjectLayout-sc-1i43xsx-0 kBtJDm'; // Применяем классы Twitch к openPanelButton для единообразного стиля openPanelButton.classList.add('ScCoreButton-sc-ocjdkq-0', 'kIbAir', 'ScButtonIcon-sc-9yap0r-0', 'eSFFfM'); openPanelButton.id = 'openPanelButton'; openPanelButton.setAttribute('aria-label', 'Toggle Control Emotes Panel'); openPanelButton.setAttribute('data-a-target', 'control-emotes-panel-toggle'); // Добавляем кнопку во внутренний контейнер innerWrapper.appendChild(openPanelButton); buttonWrapper.appendChild(innerWrapper); // Вставляем новый контейнер после кнопки "Notifications" notificationsButtonContainer.insertAdjacentElement('afterend', buttonWrapper); // Устанавливаем position: relative, чтобы кнопка следовала потоку openPanelButton.style.position = 'relative'; openPanelButton.style.top = '0'; openPanelButton.style.right = '0'; console.log('[7BTTVFZ Control Emotes Panel] Кнопка openPanelButton добавлена рядом с "Notifications"'); } // Вызываем функцию после загрузки DOM document.addEventListener('DOMContentLoaded', () => { // Ждём, пока навигационная панель загрузится const checkNav = setInterval(() => { const notificationsButton = document.querySelector('button[aria-label="Open Notifications"]'); if (notificationsButton) { addOpenPanelButtonToNav(); clearInterval(checkNav); } }, 1000); // Проверяем каждую секунду }); // ============================= end of Вставки в контейнер "penPanelButton" =============================== // //================== Блокировка и Запуск скрытия эмодзи в чате ==================// //=============== Генерация случайного ID ===============// function generateRandomID() { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const randomLength = Math.floor(Math.random() * 67) + 1; // Случайная длина от 1 до 68 let randomID = ''; for (let i = 0; i < randomLength; i++) { randomID += characters.charAt(Math.floor(Math.random() * characters.length)); } return `emote_${randomID}`; } // Оптимизированная версия toggleEmotesInNode const debouncedToggleEmotes = debounce(toggleEmotesInNode, 100); async function toggleEmotesInNode(node) { try { console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %ctoggleEmotesInNode - starting`, 'color: rgb(63, 136, 219);', 'color: rgb(52, 163, 148); font-weight: bold;'); const emotes = node.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote'); console.log(`[7BTTVFZ Control Emotes Panel] Найдено эмодзи для обработки: ${emotes.length}`); for (const emote of emotes) { const emoteUrl = emote.src || emote.getAttribute('srcset')?.split(' ')[0] || ''; const emoteAlt = emote.getAttribute('alt') || ''; let blockedEntry = null; if (emoteUrl.includes('7tv.app')) { blockedEntry = blockedEmotes.find(e => e.platform === '7tv' && e.emoteUrl === emoteUrl); } else if (emoteUrl.includes('betterttv.net')) { blockedEntry = blockedEmotes.find(e => e.platform === 'bttTV' && e.emoteUrl === emoteUrl); } else if (emoteUrl.includes('frankerfacez.com')) { blockedEntry = blockedEmotes.find(e => e.platform === 'ffz' && e.emoteUrl === emoteUrl); } else if (emoteAlt) { blockedEntry = blockedChannels.find(e => e.platform === 'TwitchChannel' && emoteAlt.startsWith(e.name)); } if (blockedEntry && !emote.getAttribute('data-emote-id')) { emote.setAttribute('data-emote-id', blockedEntry.id); } const emoteId = emote.getAttribute('data-emote-id'); const isBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId)); emote.style.display = isBlocked ? 'none' : ''; console.log(`[7BTTVFZ Control Emotes Panel] Эмодзи ${emoteAlt || emoteUrl} (ID: ${emoteId || 'не установлен'}) ${isBlocked ? 'скрыт' : 'показан'}`); } console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %ctoggleEmotesInNode - completed`, 'color: rgb(63, 136, 219);', 'color: rgb(52, 163, 148); font-weight: bold;'); } catch (error) { console.error(`[ERROR] Ошибка в toggleEmotesInNode:`, error); } } // Используем дебаунс в наблюдателе const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { console.log(`%cНовый узел добавлен в DOM`, 'color:rgb(29, 202, 136) ;'); debouncedToggleEmotes(node); } }); }); }); function observeChatContainer() { const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { // Успешно - зеленый цвет console.log( '%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата найден, начинаем наблюдение', 'color: #00C4B4; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] 'color: #00C4B4;' // Стиль для остального текста ); observer.disconnect(); // Останавливаем старое наблюдение observer.observe(chatContainer, { childList: true, subtree: true }); toggleEmotesInNode(chatContainer); // Проверяем существующие сообщения } else { // Неуспешно - красный цвет console.log( '%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден, повторная попытка через 500мс', 'color: #FF5555; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] 'color: #FF5555;' // Стиль для остального текста ); setTimeout(observeChatContainer, 500); } } // Добавляем наблюдение за изменениями на более высоком уровне DOM function startRootObserver() { const rootObserver = new MutationObserver(() => { const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); // Состояние контейнера чата - зеленый если найден, красный если не найден console.log( '%c[7BTTVFZ Control Emotes Panel]%c RootObserver: контейнер чата %c' + (chatContainer ? 'найден' : 'не найден'), 'color: #1E90FF; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] (DodgerBlue) 'color: #1E90FF;', // Стиль для "RootObserver: контейнер чата" `color: ${chatContainer ? ' #00C4B4' : ' #FF5555'}; font-weight: bold;` // Зеленый (#00C4B4) или красный (#FF5555) для статуса ); if (chatContainer) { observeChatContainer(); } }); rootObserver.observe(document.body, { childList: true, subtree: true }); // Запуск RootObserver - синий цвет (информационный) console.log( '%c[7BTTVFZ Control Emotes Panel]%c RootObserver запущен', 'color: #1E90FF; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] (DodgerBlue) 'color: #1E90FF;' // Стиль для остального текста ); } // Запускаем наблюдение startRootObserver(); let lastUrl = location.href; function checkUrlChange() { const currentUrl = location.href; if (currentUrl !== lastUrl) { console.log('[7BTTVFZ Control Emotes Panel] URL изменился, перезапускаем наблюдение за чатом'); ContextMenuManager.removeMenu(); // Удаляем контекстное меню lastUrl = currentUrl; observeChatContainer(); } setTimeout(checkUrlChange, 1000); } checkUrlChange(); //=============== Контекстное меню ===============// const contextMenuStyle = document.createElement('style'); contextMenuStyle.innerHTML = ` .custom-context-menu { position: absolute; background:rgb(19, 88, 39); border: 1px solid #ccc; padding: 5px; z-index: 10002; cursor: pointer; color: #fff; transition: background 0.3s ease; user-select: none; min-width: 150px; box-shadow: 0 2px 8px 2px #8BC34A; border-radius: 8px; } .custom-context-menu:hover { background:rgb(16, 68, 30); } `; document.head.appendChild(contextMenuStyle); const ContextMenuManager = { menu: null, isProcessing: false, // Флаг для предотвращения многократных нажатий createMenu(event, emotePrefix, platform, emoteName) { this.removeMenu(); const menu = document.createElement('div'); menu.className = 'custom-context-menu'; menu.style.top = `${event.pageY}px`; menu.style.left = `${event.pageX}px`; menu.innerText = `Block Emote (${emoteName || 'Unknown'})`; console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cContext menu created at:`, 'color: rgb(85, 113, 165);', 'color: rgb(85, 113, 165); font-weight: bold;', 'color: rgb(85, 113, 165);', event.pageX, event.pageY); document.body.appendChild(menu); this.menu = menu; menu.addEventListener('click', (e) => { e.stopPropagation(); if (this.isProcessing) return; // Пропускаем, если обработка уже идет this.isProcessing = true; console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cBlocking emote: ${emoteName}`, 'color: rgb(209, 89, 129);', 'color: rgb(255, 50, 50); font-weight: bold;', 'color: rgb(209, 89, 129);'); this.blockEmote(emotePrefix, platform, emoteName); this.removeMenu(); this.isProcessing = false; }); document.addEventListener('click', (e) => { if (!menu.contains(e.target)) this.removeMenu(); }, { once: true }); }, removeMenu() { if (this.menu) { console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cRemoving context menu`, 'color: rgb(209, 89, 129);', 'color: rgb(115, 2, 160); font-weight: bold;', 'color: white;'); this.menu.remove(); this.menu = null; } }, blockEmote(emotePrefix, platform, emoteName) { const emoteId = generateRandomID(); const currentDateTime = new Date().toISOString(); const newEntry = { id: emoteId, name: emotePrefix, // Префикс (например, "guwu") platform: platform, emoteName: emoteName || emotePrefix.split('/').pop() || 'Unknown', // Полное название (например, "guwuPopcorn") emoteUrl: platform === 'TwitchChannel' ? emotePrefix : emotePrefix, // Для Twitch используем префикс как URL date: currentDateTime }; const isDuplicate = platform === 'TwitchChannel' ? blockedChannels.some(e => e.name === newEntry.name && e.platform === newEntry.platform) : blockedEmotes.some(e => e.emoteUrl === newEntry.emoteUrl && e.platform === newEntry.platform); if (isDuplicate) { console.log(`%c[7BTTVFZ Control Emotes Panel] %cEmote already blocked: ${newEntry.emoteName}`, 'color: rgb(255, 165, 0); font-weight: bold;', 'color: rgb(255, 165, 0);'); return; } if (platform === 'TwitchChannel') { blockedChannels.push(newEntry); blockedChannelIDs.add(emoteId); newlyAddedIds.add(emoteId); GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2)); console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedChannels:`, 'color: rgb(0, 255, 0); font-weight: bold;', 'color: rgb(0, 255, 0);', newEntry); } else { blockedEmotes.push(newEntry); blockedEmoteIDs.add(emoteId); newlyAddedIds.add(emoteId); GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2)); console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedEmotes:`, 'color: rgb(0, 255, 0); font-weight: bold;', 'color: rgb(0, 255, 0);', newEntry); } const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { toggleEmotesInNode(chatContainer); } updateBlockedList(); updateCounter(); } }; //=============== Обработчик контекстного меню ===============// // блокировка сайлов правой кнопкой // document.addEventListener('contextmenu', (event) => { const target = event.target; if (target.tagName === 'IMG' && target.closest('.chat-line__message')) { event.preventDefault(); const emoteUrl = target.src || target.getAttribute('srcset')?.split(' ')[0] || ''; const emoteAlt = target.getAttribute('alt') || ''; const dataProvider = target.getAttribute('data-provider') || ''; let emotePrefix = ''; let platform = ''; let emoteName = emoteAlt; console.log(`[${new Date().toISOString()}] [7BTTVFZ Control Emotes Panel] Context menu triggered for:`, emoteUrl, emoteAlt, 'data-provider:', dataProvider); // Определяем платформу и префикс if (dataProvider === 'bttv' && emoteUrl.includes('betterttv.net')) { emotePrefix = emoteUrl || `https://cdn.betterttv.net/emote/${target.getAttribute('data-id')}/2x.webp`; platform = 'bttTV'; console.log("[7BTTVFZ Control Emotes Panel] Detected bttv emote (via data-provider):", emotePrefix); } else if (dataProvider === 'ffz' && emoteUrl.includes('frankerfacez.com')) { emotePrefix = emoteUrl || `https://cdn.frankerfacez.com/emote/${target.getAttribute('data-id')}/2`; platform = 'ffz'; emoteName = emoteAlt; console.log("[7BTTVFZ Control Emotes Panel] Detected ffz emote (via data-provider):", emotePrefix); } else if (dataProvider === 'ffz' && emoteUrl.includes('7tv.app')) { emotePrefix = emoteUrl || `https://cdn.7tv.app/emote/${target.getAttribute('data-id')}/2x.webp`; platform = '7tv'; console.log("[7BTTVFZ Control Emotes Panel] Detected 7tv emote (via data-provider):", emotePrefix); } else if (emoteUrl.includes('betterttv.net')) { emotePrefix = emoteUrl; platform = 'bttTV'; console.log("[7BTTVFZ Control Emotes Panel] Detected bttv emote (via URL):", emoteUrl); } else if (emoteUrl.includes('7tv.app')) { emotePrefix = emoteUrl; platform = '7tv'; console.log("[7BTTVFZ Control Emotes Panel] Detected 7tv emote (via URL):", emoteUrl); } else if (emoteUrl.includes('frankerfacez.com')) { emotePrefix = emoteUrl; platform = 'ffz'; emoteName = emoteAlt; console.log("[7BTTVFZ Control Emotes Panel] Detected ffz emote (via URL):", emoteUrl); } else if (emoteAlt) { // Обновленная логика для TwitchChannel const match = emoteAlt.match(/^([a-z0-9]+)([A-Z].*)$/); // Ищем префикс до первой заглавной буквы if (match) { emotePrefix = match[1]; // Например, "lowti3" из "lowti3Face3" emoteName = emoteAlt; // Полное название, например "lowti3Face3" } else { // Если не удалось разделить, используем первую группу символов до не-букв/цифр как запасной вариант emotePrefix = emoteAlt.split(/[^a-zA-Z0-9]/)[0] || emoteAlt; emoteName = emoteAlt; } platform = 'TwitchChannel'; console.log("[7BTTVFZ Control Emotes Panel] Detected TwitchChannel emote:", emoteAlt, "prefix:", emotePrefix); } if (emotePrefix && platform) { console.log(`[7BTTVFZ Control Emotes Panel] Creating context menu for emote with prefix: ${emotePrefix}, platform: ${platform}`); ContextMenuManager.createMenu(event, emotePrefix, platform, emoteName); } else { console.log("[7BTTVFZ Control Emotes Panel] Could not determine platform or prefix, using fallback TwitchChannel"); ContextMenuManager.createMenu(event, emoteAlt || emoteUrl, 'TwitchChannel', emoteAlt || 'Unknown'); } } }); //=============== Запуск ===============// observeChatContainer(); //====================== Управление высотой панели ======================= function closePanel() { isPanelOpen = false; GM_setValue('isPanelOpen', isPanelOpen); controlPanel.style.height = '0px'; // Плавно уменьшаем высоту setTimeout(() => { if (!isPanelOpen) controlPanel.style.display = 'none'; // Полностью скрываем после завершения анимации }, 150); // Таймер соответствует времени анимации } //----------------- Анимация сворачивания панели------------------------- function openPanel() { isPanelOpen = true; GM_setValue('isPanelOpen', isPanelOpen); controlPanel.style.display = 'block'; // Делаем панель видимой setTimeout(() => { controlPanel.style.height = '656px'; // Плавно увеличиваем высоту }, 0); // Устанавливаем высоту с задержкой для работы анимации } //========================== Переключение состояния панели Управления 'openPanelButton' =============================== openPanelButton.onclick = () => { isPanelOpen = !isPanelOpen; // Переключаем состояние панели (открыта/закрыта) GM_setValue('isPanelOpen', isPanelOpen); // Перемещаем переключатель (круглый элемент), когда панель открывается или закрывается switchCircle.style.transform = isPanelOpen ? 'translateX(20px)' : 'translateX(0)'; // Меняем цвет фона контейнера в зависимости от состояния панели // switchContainer.style.backgroundColor = isPanelOpen ? ' #9e9e9e' : ' #171c1c'; // // закоментируем убрав временно для будущих версий switchContainer // // Переключаем видимость панели: открываем или закрываем if (isPanelOpen) { openPanel(); // Вызов функции для открытия панели } else { closePanel(); // Вызов функции для закрытия панели } }; // Инициализация состояния updateSwitchState(); // Убедимся, что переключатель синхронизирован с начальным состоянием updateBlockedList(); updateCounter(); //============== Минипанель с кнопками сортировки по категориям =================// const sortContainer = document.createElement('div'); sortContainer.id = 'sortContainer'; sortContainer.style.display = 'flex'; sortContainer.style.justifyContent = 'space-around'; sortContainer.style.backgroundColor = ' rgb(89 51 114)'; sortContainer.style.padding = '5px'; sortContainer.style.marginBottom = '37px'; sortContainer.style.position = 'relative'; sortContainer.style.top = '57px'; sortContainer.style.borderRadius = '8px 8px 0 0'; // Закругление только верхних углов sortContainer.style.border = '1px solid rgb(255, 255, 255)'; sortContainer.style.boxShadow = ' rgb(0 0 0 / 0%) 0px 15px 6px 0px'; // Использование RGBA для прозрачности sortContainer.style.zIndex = 'inherit'; // Наследует z-index от родителя // Определение начальных значений для currentSortOrder let currentSortOrder = { name: 'asc', platform: 'asc', date: 'asc' }; // Кнопки сортировки const sortByNameButton = document.createElement('button'); sortByNameButton.innerHTML = 'Name ▲'; sortByNameButton.style.cursor = 'pointer'; sortByNameButton.style.position = 'relative'; sortByNameButton.style.left = '13%'; sortByNameButton.onclick = () => { const order = currentSortOrder.name === 'asc' ? 'desc' : 'asc'; currentSortOrder.name = order; sortByNameButton.innerHTML = `Name ${order === 'asc' ? '▲' : '▼'}`; // Переключение стрелочки sortblockedEmotes('name', order); }; sortContainer.appendChild(sortByNameButton); const sortByPlatformButton = document.createElement('button'); sortByPlatformButton.innerHTML = 'Platform ▲'; sortByPlatformButton.style.cursor = 'pointer'; sortByPlatformButton.style.position = 'relative'; sortByPlatformButton.style.right = '118px'; sortByPlatformButton.onclick = () => { const order = currentSortOrder.platform === 'asc' ? 'desc' : 'asc'; currentSortOrder.platform = order; sortByPlatformButton.innerHTML = `Platform ${order === 'asc' ? '▲' : '▼'}`; sortblockedEmotes('platform', order); }; sortContainer.appendChild(sortByPlatformButton); const sortByDateButton = document.createElement('button'); sortByDateButton.innerHTML = 'Date-Time ▲'; sortByDateButton.style.cursor = 'pointer'; sortByDateButton.style.top = '0px'; sortByDateButton.style.position = 'relative'; sortByDateButton.style.left = '9px'; sortByDateButton.onclick = () => { const order = currentSortOrder.date === 'asc' ? 'desc' : 'asc'; currentSortOrder.date = order; sortByDateButton.innerHTML = `Date ${order === 'asc' ? '▲' : '▼'}`; sortblockedEmotes('date', order); }; sortContainer.appendChild(sortByDateButton); // Добавляем контейнер сортировки в панель управления controlPanel.insertBefore(sortContainer, title); // ---------- goToLast Button ------------- // const goToLastButton = document.createElement('button'); goToLastButton.innerHTML = 'Go To Last Element ▼'; // Короткое название goToLastButton.style.cursor = 'pointer'; goToLastButton.style.position = 'relative'; goToLastButton.style.right = '2%'; // Сдвигаем чуть левее для баланса goToLastButton.onclick = () => { goToLastAddedItem(); }; sortContainer.appendChild(goToLastButton); sortContainer.appendChild(showStatsButton); //============== Функция для сортировки списка =================// function sortblockedEmotes(criteria, order) { const sortFunc = (a, b) => { let comparison = 0; if (criteria === 'name') { comparison = a.emoteName.localeCompare(b.emoteName); } else if (criteria === 'platform') { comparison = a.platform.localeCompare(b.platform); } else if (criteria === 'date') { comparison = new Date(a.date) - new Date(b.date); } return order === 'asc' ? comparison : -comparison; }; // Сортируем оба массива blockedEmotes.sort(sortFunc); blockedChannels.sort(sortFunc); // Обновляем интерфейс после сортировки updateBlockedList(); } //============== Обработчики событий для кнопок =================// const buttons = [addButton, clearAllButton, exportButton, importButton, unblockAllButton, blockAllButton]; buttons.forEach(button => { button.onmouseover = function() { button.style.background = '-webkit-linear-gradient(135deg, #443157 0%,rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)'; // Изменение фона при наведении }; button.onmouseout = function() { button.style.background = buttonColor; // Возвращаем исходный цвет }; }); // ========= Функция для прокрутки к последнему добавленному элементу ============= // function goToLastAddedItem() { const allItems = [...blockedEmotes, ...blockedChannels]; if (allItems.length === 0) { console.log("[7BTTVFZ Control Emotes Panel] Список пуст, некуда прокручивать"); return; } // Находим элемент с самой поздней датой const lastItem = allItems.reduce((latest, current) => { return new Date(current.date) > new Date(latest.date) ? current : latest; }); // Ищем элемент в DOM по его ID let lastElement = list.querySelector(`[data-id="${lastItem.id}"]`); if (lastElement) { // Подсвечиваем элемент lastElement.classList.add('last-item-highlight'); // Прокручиваем к элементу const itemOffsetTop = lastElement.offsetTop; const listHeight = list.clientHeight; const itemHeight = lastElement.clientHeight; const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2); list.scrollTo({ top: scrollPosition, behavior: 'smooth' }); // Убираем подсветку через 60 секунд setTimeout(() => { lastElement.classList.remove('last-item-highlight'); console.log(`[7BTTVFZ Control Emotes Panel] Подсветка убрана с элемента: ${lastItem.emoteName}`); }, 60000); // 60000 мс = 1 минута console.log(`[7BTTVFZ Control Emotes Panel] Прокручено и подсвечено: ${lastItem.emoteName} (ID: ${lastItem.id})`); } else { console.log("[7BTTVFZ Control Emotes Panel] Последний элемент не найден в DOM, обновляем список"); updateBlockedList(); setTimeout(() => { lastElement = list.querySelector(`[data-id="${lastItem.id}"]`); if (lastElement) { lastElement.classList.add('last-item-highlight'); const itemOffsetTop = lastElement.offsetTop; const listHeight = list.clientHeight; const itemHeight = lastElement.clientHeight; const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2); list.scrollTo({ top: scrollPosition, behavior: 'smooth' }); setTimeout(() => { lastElement.classList.remove('last-item-highlight'); console.log(`[7BTTVFZ Control Emotes Panel] Подсветка убрана с элемента после обновления: ${lastItem.emoteName}`); }, 60000); console.log(`[7BTTVFZ Control Emotes Panel] Успешно прокручено и подсвечено после обновления: ${lastItem.emoteName}`); } }, 100); } } console.log(getComputedStyle(controlPanel).display); console.log("[7BTTVFZ Control Emotes Panel] Opening control panel..."); console.log("[7BTTVFZ Control Emotes Panel] Creating control panel..."); console.log("[7BTTVFZ Control Emotes Panel] Adding button..."); console.log("[7BTTVFZ Control Emotes Panel] Updating channel list..."); console.log("[7BTTVFZ Control Emotes Panel] Creating file input element..."); // Удаляем некорректные логи с event, так как они не в контексте события console.log("[7BTTVFZ Control Emotes Panel] Processing imported channels..."); console.log("[7BTTVFZ Control Emotes Panel] Updating interface..."); console.log("[7BTTVFZ Control Emotes Panel] Showing all emotes in chat..."); console.log("[7BTTVFZ Control Emotes Panel] Blocking all emotes..."); console.log("[7BTTVFZ Control Emotes Panel] Hiding emotes for a channel..."); console.log(`%c[7BTTVFZ Control Emotes Panel] %cWaiting for chat container...`, 'color:rgb(255, 114, 173); font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] 'color: rgb(255, 114, 173) ;'); // Стиль для остального текста console.log("[7BTTVFZ Control Emotes Panel] Creating context menu..."); // Добавляем переменные для отслеживания состояния let lastKnownBlockedCount = blockedEmotes.length + blockedChannels.length; let lastCheckTime = Date.now(); let isRestarting = false; // Функция проверки состояния блокировки function checkBlockingStatus() { console.log(`%c[WATCHDOG] %cПроверка состояния блокировки...`, 'color:rgb(221, 101, 175); font-weight: bold;', 'color: rgb(164, 207, 44) ;'); const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (!chatContainer) { console.log( "%c[WATCHDOG]%c Контейнер чата не найден, перезапускаем наблюдение", 'color:rgb(172, 147, 223); font-weight: bold;', 'color: rgb(164, 207, 44) ;'); observeChatContainer(); // Перезапускаем наблюдение return false; } const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote'); if (emotes.length === 0) { console.log("[WATCHDOG] Эмодзи в чате не найдены, пропускаем проверку"); return true; } let failureDetected = false; emotes.forEach((emote, index) => { if (index > 5) return; const emoteId = emote.getAttribute('data-emote-id'); const shouldBeBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId)); const isVisible = emote.style.display !== 'none'; if (shouldBeBlocked && isVisible) { console.log(`[WATCHDOG] Обнаружен сбой: эмодзи с ID ${emoteId} должен быть скрыт, но виден!`); failureDetected = true; } else if (!shouldBeBlocked && !isVisible) { console.log(`[WATCHDOG] Обнаружен сбой: эмодзи с ID ${emoteId} не должен быть скрыт, но скрыт!`); failureDetected = true; } }); const currentBlockedCount = blockedEmotes.length + blockedChannels.length; if (currentBlockedCount !== lastKnownBlockedCount) { console.log( `%c[WATCHDOG] %cКоличество заблокированных элементов изменилось: %c${lastKnownBlockedCount} -> ${currentBlockedCount}`, 'color: rgb(221, 101, 175); font-weight: bold;', 'color: rgb(164, 207, 44);', 'color: rgb(255, 165, 0); font-weight: bold;' ); lastKnownBlockedCount = currentBlockedCount; } return !failureDetected; } function showNotification(message, duration = 3000) { const notification = document.createElement('div'); notification.innerText = message; // Добавляем текст notification.style.position = 'relative'; notification.style.bottom = '99%'; notification.style.maxWidth = '155px'; notification.style.left = '61%'; notification.style.backgroundColor = ' #341d41'; notification.style.color = ' #30aa54'; notification.style.padding = '6px'; notification.style.borderRadius = '40px'; notification.style.boxShadow = 'rgb(130, 113, 148) 1px 1px 7px 4px'; notification.style.zIndex = '1001'; notification.style.fontSize = '10px'; // Начальные стили для анимации (уменьшенный размер) notification.style.transform = 'scale(0)'; // Начинаем с масштаба 0 notification.style.opacity = '0'; // Начинаем с прозрачности 0 notification.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; // Плавный переход для масштаба и прозрачности document.body.appendChild(notification); // Запускаем анимацию увеличения после добавления в DOM setTimeout(() => { notification.style.transform = 'scale(1)'; // Увеличиваем до нормального размера notification.style.opacity = '1'; // Делаем полностью видимым }, 10); // Небольшая задержка для запуска перехода // Удаляем уведомление после завершения длительности setTimeout(() => { // Добавляем анимацию исчезновения перед удалением (опционально) notification.style.transform = 'scale(0)'; notification.style.opacity = '0'; setTimeout(() => { notification.remove(); }, 300); // Соответствует времени transition }, duration); } // Функция перезапуска логики блокировки function restartBlockingLogic() { if (isRestarting) return; isRestarting = true; // Перезапуск логики - оранжевый цвет (в процессе) console.log( '%c[WATCHDOG]%c Перезапуск логики блокировки...', 'color: #FF4500; font-weight: bold;', // Стиль для [WATCHDOG] (OrangeRed) 'color: #FF4500;' // Стиль для остального текста ); showNotification(" chat not found ... waiting... ", 3000); // уведомление о перезапуске когда сбой failure observer.disconnect(); const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (chatContainer) { const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote'); emotes.forEach(emote => { emote.style.display = ''; emote.removeAttribute('data-emote-id'); }); observeChatContainer(); toggleEmotesInNode(chatContainer); } else { observeChatContainer(); } updateBlockedList(); updateCounter(); setTimeout(() => { isRestarting = false; // Перезапуск завершен - зеленый цвет (успех) console.log( '%c[WATCHDOG]%c Перезапуск завершен', 'color: #00C4B4; font-weight: bold;', // Стиль для [WATCHDOG] (Teal) 'color: #00C4B4;' // Стиль для остального текста ); }, 1000); // Задержка для предотвращения спама } // Периодическая проверка состояния (watchdog) function startWatchdog() { const interval = GM_getValue('watchdogInterval', 10) * 1000; // Секунды в миллисекунды setInterval(() => { const currentTime = Date.now(); if (currentTime - lastCheckTime < 5000) return; lastCheckTime = currentTime; const isWorking = checkBlockingStatus(); if (!isWorking) { console.log( '%c[WATCHDOG]%c Обнаружен сбой в работе блокировки, перезапуск...', 'color: #FFA500; font-weight: bold;', 'color: #FFA500;' ); restartBlockingLogic(); } else { console.log( `%c[WATCHDOG] %cБлокировка работает корректно!`, 'color:rgb(6, 167, 0); font-weight: bold;', 'color: rgb(164, 207, 44);' ); } }, interval); // Используем динамический интервал } //================ Модуль управления темами ================== // (function () { // Определяем начальный массив тем //================ Модуль управления темами ================== // (function () { // Резервные темы (используются, если загрузка с GitHub не удалась) const fallbackThemes = [ { name: 'default', displayName: 'Default Theme', styles: { controlPanel: { background: '-webkit-linear-gradient(270deg, hsla(50, 76%, 56%, 1) 0%, hsla(32, 83%, 49%, 1) 25%, hsla(0, 37%, 37%, 1) 59%, hsla(276, 47%, 24%, 1) 79%, hsla(261, 11%, 53%, 1) 100%)', border: '1px solid #ccc', boxShadow: 'rgb(0 0 0) 0px 18px 29px 3px', color: ' #fff' }, lastItemHighlight: { backgroundColor: ' #ffd700' }, openPanelButton: { background: ' #4c2a5e', color: ' #bda3d7', border: 'none' }, switchContainer: { backgroundColor: ' #ccb8eb5c', activeBackgroundColor: ' #bda3d7' }, switchCircle: { backgroundColor: ' #4c2a5e', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.8)' }, list: { background: '-webkit-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)', border: '1px solid #ffffff', color: ' #fff', scrollbarThumb: ' #C1A5EF', scrollbarTrack: ' #455565' }, counter: { backgroundColor: ' #b69dcf', color: ' #4c2a5e', border: '3px solid #4c2a5e' }, sortContainer: { backgroundColor: 'rgb(89, 51, 114)', border: '1px solid rgb(255, 255, 255)', color: ' #fff' }, title: { color: ' #2a1e38' }, buttons: { background: ' #907cad', color: ' #fff' }, versionLabel: { color: 'rgb(62, 33, 85)' }, searchInput: { background: ' #192427', color: ' #b69dcf', border: '1px solid #b69dcf' }, input: { background: ' #192427', color: ' #b69dcf', border: '1px solid #b69dcf' }, themeSelect: { background: ' #192427', color: ' #b69dcf', border: '1px solid #c1a5ef' }, platformSelect: { background: ' #192427', color: ' #b69dcf', border: '1px solid #c1a5ef' }, deleteButton: { background: ' #944646', color: ' #fff', hoverBackground: 'linear-gradient(135deg, #480a0c 56%, #ca5d5f 98%, #8b4040 100%)' }, listItemText: { color: ' #ffffff' }, listItemLink: { color: ' #b3e0f2' }, listItemDate: { color: ' #cccccc' }, chartWrapper: { backgroundColor: ' #fff', border: '1px solid #ffffff', color: ' #000' } } } ]; // Загружаем кэшированные темы или используем резервные let themes = GM_getValue('themes', fallbackThemes); let selectedThemeName = GM_getValue('selectedTheme', 'default'); // Функция для загрузки тем с GitHub async function loadThemesFromGitHub() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: 'https://raw.githubusercontent.com/sopernik566/Control_Emotes_Panel_Twitch_JS/refs/heads/main/themes.json', onload: function (response) { try { const loadedThemes = JSON.parse(response.responseText); console.log('[7BTV Control Emotes Panel] Темы загружены с GitHub:', loadedThemes); themes = loadedThemes; GM_setValue('themes', themes); // Кэшируем темы локально resolve(themes); } catch (err) { console.error('[7BTV Control Emotes Panel] Ошибка парсинга тем с GitHub:', err); reject(err); } }, onerror: function (err) { console.error('[7BTV Control Emotes Panel] Ошибка загрузки тем с GitHub:', err); reject(err); } }); }); } // Функция для сохранения тем в хранилище function saveThemes() { GM_setValue('themes', themes); console.log('[7BTV Control Emotes Panel] Темы сохранены в хранилище:', themes); } // Функция для сохранения выбранной темы function saveSelectedTheme(themeName) { selectedThemeName = themeName; GM_setValue('selectedTheme', themeName); console.log('[7BTV Control Emotes Panel] Выбранная тема сохранена:', themeName); } // Функция для применения темы function applyTheme(themeName) { const theme = themes.find(t => t.name === themeName) || themes[0]; if (!theme) { console.warn(`[7BTV Control Emotes Panel] Тема ${themeName} не найдена, используется 'default'`); applyTheme('default'); return; } console.log(`[7BTV Control Emotes Panel] Применение темы: ${themeName}`); const currentPanelDisplay = controlPanel.style.display; const currentPanelHeight = controlPanel.style.height; if (openPanelButton) { Object.assign(openPanelButton.style, theme.styles.openPanelButton); openPanelButton.style.position = 'fixed'; openPanelButton.style.zIndex = '10000'; openPanelButton.style.transition = 'background 0.3s ease'; } if (switchContainer) { Object.assign(switchContainer.style, { backgroundColor: isPanelOpen ? theme.styles.switchContainer.activeBackgroundColor : theme.styles.switchContainer.backgroundColor, width: '44px', height: '27px', borderRadius: '13px', position: 'relative', transition: 'background 0.3s ease' }); } if (switchCircle) { Object.assign(switchCircle.style, theme.styles.switchCircle); switchCircle.style.width = '19px'; switchCircle.style.height = '19px'; switchCircle.style.borderRadius = '50%'; switchCircle.style.position = 'absolute'; switchCircle.style.top = '3px'; switchCircle.style.left = '3px'; switchCircle.style.transition = 'transform 0.3s ease'; } if (controlPanel) { Object.assign(controlPanel.style, theme.styles.controlPanel); controlPanel.style.display = currentPanelDisplay; controlPanel.style.height = currentPanelHeight; controlPanel.style.transition = 'height 0.3s ease'; } if (list) { Object.assign(list.style, theme.styles.list); const styleElement = document.getElementById('customScrollbarStyle') || document.createElement('style'); styleElement.id = 'customScrollbarStyle'; styleElement.innerHTML = ` #blockedList::-webkit-scrollbar { width: 25px; } #blockedList::-webkit-scrollbar-thumb { background-color: ${theme.styles.list.scrollbarThumb}; border-radius: 8px; border: 3px solid #4F3E6A; height: 80px; } #blockedList::-webkit-scrollbar-thumb:hover { background-color: ${theme.styles.list.scrollbarThumb}; } #blockedList::-webkit-scrollbar-thumb:active { background-color: ${theme.styles.list.scrollbarThumb}; } #blockedList::-webkit-scrollbar-track { background: ${theme.styles.list.scrollbarTrack}; border-radius: 0px 0px 8px 0px; } #blockedList::-webkit-scrollbar-track:hover { background: ${theme.styles.list.scrollbarTrack}; } #blockedList::-webkit-scrollbar-track:active { background: ${theme.styles.list.scrollbarTrack}; } `; if (!document.getElementById('customScrollbarStyle')) { document.head.appendChild(styleElement); } } const lastItemHighlightStyle = document.createElement('style'); lastItemHighlightStyle.id = 'lastItemHighlightStyle'; lastItemHighlightStyle.innerHTML = ` .last-item-highlight { background-color: ${theme.styles.lastItemHighlight?.backgroundColor || ' #ffd700'}; transition: background-color 0.5s ease; } `; const existingStyle = document.getElementById('lastItemHighlightStyle'); if (existingStyle) { existingStyle.remove(); } document.head.appendChild(lastItemHighlightStyle); if (counter) { Object.assign(counter.style, theme.styles.counter); counter.style.display = 'flex'; } if (sortContainer) { Object.assign(sortContainer.style, theme.styles.sortContainer); sortContainer.style.display = 'flex'; } if (title) { Object.assign(title.style, theme.styles.title); title.style.display = 'block'; } const buttons = [addButton, clearAllButton, exportButton, importButton, unblockAllButton, blockAllButton, searchButton]; buttons.forEach(button => { Object.assign(button.style, theme.styles.buttons); button.onmouseover = () => { button.style.background = '-webkit-linear-gradient(135deg, #443157 0%, rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)'; }; button.onmouseout = () => { button.style.background = theme.styles.buttons.background; }; }); if (versionLabel) { Object.assign(versionLabel.style, theme.styles.versionLabel); } if (searchInput) { Object.assign(searchInput.style, theme.styles.searchInput); } if (input) { Object.assign(input.style, theme.styles.input); } if (platformSelect) { Object.assign(platformSelect.style, theme.styles.platformSelect); } if (themeSelect) { Object.assign(themeSelect.style, theme.styles.themeSelect); } const deleteButtons = list.querySelectorAll('.delete-button'); deleteButtons.forEach(button => { Object.assign(button.style, theme.styles.deleteButton, { height: '35px', width: '75px', fontWeight: 'bold', fontSize: '16px', border: 'none', borderRadius: '4px', cursor: 'pointer', boxShadow: '0 4px 8px rgba(0, 0, 0, 0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.3s ease' }); button.onmouseover = () => { button.style.background = theme.styles.deleteButton.hoverBackground; }; button.onmouseout = () => { button.style.background = theme.styles.deleteButton.background; }; }); currentDeleteButtonStyles = { background: theme.styles.deleteButton.background, color: theme.styles.deleteButton.color, hoverBackground: theme.styles.deleteButton.hoverBackground }; console.log('[7BTV Control Emotes Panel] Сохранены стили кнопки Delete:', currentDeleteButtonStyles); if (list) { const listItemTexts = list.querySelectorAll('.list-item-text'); const listItemLinks = list.querySelectorAll('.list-item-link'); const listItemDates = list.querySelectorAll('.list-item-date'); listItemTexts.forEach(span => { Object.assign(span.style, theme.styles.listItemText); }); listItemLinks.forEach(span => { Object.assign(span.style, theme.styles.listItemLink); }); listItemDates.forEach(span => { Object.assign(span.style, theme.styles.listItemDate); }); } saveSelectedTheme(themeName); } // Функция для обновления селектора тем function updateThemeSelector() { themeSelect.innerHTML = ''; // Очищаем текущие опции themes.forEach(theme => { const option = document.createElement('option'); option.value = theme.name; option.innerText = theme.displayName; if (theme.name === selectedThemeName) { option.selected = true; } themeSelect.appendChild(option); }); } // Инициализация интерфейса выбора тем const themeSelectorContainer = document.createElement('div'); themeSelectorContainer.style.position = 'relative'; themeSelectorContainer.style.bottom = '100px'; themeSelectorContainer.style.width = '126px'; themeSelectorContainer.style.left = '0px'; themeSelectorContainer.style.zIndex = '10001'; const themeSelect = document.createElement('select'); themeSelect.style.padding = '5px'; themeSelect.style.height = '35px'; themeSelect.style.width = '126px'; themeSelect.style.borderRadius = '4px'; themeSelect.style.background = ' #192427'; themeSelect.style.color = ' #b69dcf'; themeSelect.style.border = '1px solid #b69dcf'; themeSelect.onchange = () => { const selectedTheme = themeSelect.value; applyTheme(selectedTheme); }; themeSelectorContainer.appendChild(themeSelect); controlPanel.appendChild(themeSelectorContainer); // Загружаем темы с GitHub и инициализируем интерфейс loadThemesFromGitHub() .then(() => { updateThemeSelector(); applyTheme(selectedThemeName); }) .catch(() => { console.warn('[7BTV Control Emotes Panel] Не удалось загрузить темы с GitHub, использую кэшированные или резервные'); updateThemeSelector(); applyTheme(selectedThemeName); }); // Запускаем watchdog startWatchdog(); })(); })()})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址