Control Emotes Panel 2.6.52 (C) tapeavion

Twitch emoji blocking how to block emotes in twitch chat

  1. // ==UserScript==
  2. // @name Control Emotes Panel 2.6.52 (C) tapeavion
  3. // @version 2.6.52
  4. // @description Twitch emoji blocking how to block emotes in twitch chat
  5. // @author Gullampis810
  6. // @license Proprietary (c) tapeavion
  7. // @match https://www.twitch.tv/*
  8. // @match https://blank.org/*
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_unregisterMenuCommand
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @connect raw.githubusercontent.com
  14. // @grant GM_xmlhttpRequest
  15. // @icon https://raw.githubusercontent.com/sopernik566/icons/refs/heads/main/7bttvffzTtvlogo.ico
  16. // @namespace http://tampermonkey.net/
  17. // @tag twitch
  18. // @tag emote-blocker
  19. // @tag chat-filter
  20. // @tag block-emotes
  21. // @tag twitch-chat
  22. // @tag 7tv
  23. // @tag bttv
  24. // @tag ffz
  25. // @tag twitch-channel
  26. // @tag how to block emotes in twitch chat
  27. // @tag how ban all emotes
  28. // @tag block all emotes
  29. // ==/UserScript==
  30.  
  31.  
  32. (function () {
  33. 'use strict';
  34.  
  35. // Подключаем Chart.js через CDN
  36. const chartJsScript = document.createElement('script');
  37. chartJsScript.src = 'https://cdn.jsdelivr.net/npm/chart.js';
  38. chartJsScript.async = true;
  39. document.head.appendChild(chartJsScript);
  40.  
  41.  
  42. // === объект для хранения текущих стилей кнопки DeleteButton в случае пересоздания и сброса стиля //
  43. let currentDeleteButtonStyles = {
  44. background: 'rgb(168, 77, 77)', // Начальный цвет из hoverStyle
  45. color: '#fff',
  46. hoverBackground: 'linear-gradient(135deg, #f75557 0%, #480a0c 56%, #4e1314 98%, #ff4d4d 100%)'
  47. };
  48.  
  49. let blockedEmotes = [];
  50. let blockedChannels = [];
  51.  
  52. // глобальное определение для поисковой строки
  53. function debounce(func, wait) {
  54. let timeout;
  55. return function (...args) {
  56. clearTimeout(timeout);
  57. timeout = setTimeout(() => func.apply(this, args), wait);
  58. };
  59. }
  60.  
  61. // Функция для безопасного получения и парсинга данных
  62. function loadData(key, defaultValue) {
  63. const rawData = GM_getValue(key, defaultValue);
  64. try {
  65. return typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
  66. } catch (e) {
  67. console.error(`Ошибка при парсинге ${key}:`, e);
  68. return defaultValue; // Возвращаем значение по умолчанию в случае ошибки
  69. }
  70. }
  71.  
  72. // Загружаем данные при старте
  73. blockedEmotes = loadData("blockedEmotes", []);
  74. blockedChannels = loadData("blockedChannels", []);
  75. console.log("[7BTTVFZ Control Emotes Panel] Загружены blockedEmotes:", blockedEmotes);
  76. console.log("[7BTTVFZ Control Emotes Panel] Загружены blockedChannels:", blockedChannels);
  77.  
  78. let isPanelOpen = GM_getValue('isPanelOpen', false);
  79.  
  80.  
  81.  
  82. //=== Функция для перемещения панели ===//
  83. function makePanelDraggable(panel) {
  84. let offsetX = 0, offsetY = 0, isDragging = false;
  85.  
  86. // Создаем заголовок, за который можно перетаскивать
  87. const dragHandle = document.createElement('div');
  88. dragHandle.style.width = '100%';
  89. dragHandle.style.height = '656px';
  90. dragHandle.style.background = 'rgba(0, 0, 0, 0.0)';
  91. dragHandle.style.cursor = 'grab';
  92. dragHandle.style.position = 'absolute';
  93. dragHandle.style.top = '0';
  94. dragHandle.style.left = '0';
  95. dragHandle.style.zIndex = '-1';
  96. dragHandle.style.borderRadius = '8px 8px 0 0';
  97. panel.appendChild(dragHandle);
  98.  
  99. // Начало перемещения
  100. dragHandle.addEventListener('mousedown', (e) => {
  101. isDragging = true;
  102. offsetX = e.clientX - panel.getBoundingClientRect().left;
  103. offsetY = e.clientY - panel.getBoundingClientRect().top;
  104. dragHandle.style.cursor = 'grabbing';
  105. });
  106.  
  107. // Перемещение панели
  108. document.addEventListener('mousemove', (e) => {
  109. if (!isDragging) return;
  110. panel.style.left = `${e.clientX - offsetX}px`;
  111. panel.style.top = `${e.clientY - offsetY}px`;
  112. });
  113.  
  114. // Остановка перемещения
  115. document.addEventListener('mouseup', () => {
  116. isDragging = false;
  117. dragHandle.style.cursor = 'grab';
  118. });
  119. }
  120.  
  121. //===================================== Панель управления =======================================//
  122. const controlPanel = document.createElement('div');
  123. controlPanel.style.position = 'fixed'; // Фиксируем панель на экране
  124. controlPanel.style.bottom = '124px'; // Располагаем панель на 124px от нижней границы экрана
  125. controlPanel.style.right = '380px'; // Располагаем панель на 310px от правой границы экрана
  126. controlPanel.style.width = '690px'; // Ширина панели
  127. controlPanel.style.height = '656px'; // Высота панели
  128. controlPanel.style.backgroundColor = '#5c5065'; // Цвет фона панели
  129. 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%)'; // Применяем градиентный фон
  130.  
  131. controlPanel.style.border = '1px solid #ccc'; // Цвет и стиль границы панели
  132. controlPanel.style.borderRadius = '8px'; // Скругляем углы панели
  133. controlPanel.style.padding = '10px'; // Отступы внутри панели
  134. controlPanel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; // Добавляем тень панели
  135. controlPanel.style.zIndex = 10000; // Устанавливаем высокий z-index, чтобы панель была поверх других элементов
  136. controlPanel.style.fontFamily = 'Arial, sans-serif'; // Шрифт текста на панели
  137. controlPanel.style.transition = 'height 0.3s ease'; // Плавное изменение высоты при изменении
  138. controlPanel.style.overflow = 'hidden'; // Скрытие содержимого, если оно выходит за пределы панели
  139.  
  140.  
  141. // Метка версии внизу панели
  142. const versionLabel = document.createElement('div');
  143. versionLabel.innerText = 'v.2.6.52';
  144. versionLabel.style.position = 'absolute';
  145. versionLabel.style.top = '4px';
  146. versionLabel.style.right = '610px';
  147. versionLabel.style.color = ' #3e2155';
  148. versionLabel.style.fontSize = '12px';
  149. versionLabel.style.fontFamily = 'Arial, sans-serif';
  150. versionLabel.style.fontWeight = 'bold';
  151. controlPanel.appendChild(versionLabel);
  152.  
  153.  
  154. // Добавляем панель в DOM и активируем перетаскивание
  155. document.body.appendChild(controlPanel);
  156. makePanelDraggable(controlPanel);
  157.  
  158.  
  159. // ======== Создаём кнопку шестерёнки для настроек ======== //
  160. const settingsButton = document.createElement('button');
  161. 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;">';
  162. settingsButton.style.width = '28px';
  163. settingsButton.style.height = '28px';
  164. settingsButton.style.position = 'absolute';
  165. settingsButton.style.top = '3px';
  166. settingsButton.style.right = '657px';
  167. settingsButton.style.background = 'transparent';
  168. settingsButton.style.border = 'none';
  169. settingsButton.style.color = '#fff';
  170. settingsButton.style.cursor = 'pointer';
  171. settingsButton.style.zIndex = '10001';
  172. settingsButton.title = getTranslation('settingsTitle');
  173. controlPanel.appendChild(settingsButton);
  174.  
  175. // Добавляем CSS для попапа и анимации
  176. const styleSheet = document.createElement('style');
  177. styleSheet.textContent = `
  178. .settings-popup {
  179. position: fixed;
  180. top: 20%;
  181. left: 30%;
  182. width: 40%;
  183. height: 60%;
  184. background: linear-gradient(181deg, hsla(50, 76%, 56%, 1) 0%, hsla(0, 37%, 37%, 1) 59%, hsla(276, 47%, 24%, 1) 79%);
  185. border: 1px solid #bda3d7;
  186. border-radius: 12px;
  187. z-index: 10002;
  188. padding: 10px;
  189. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  190. box-shadow: 6px 12px 20px 9px rgb(0 0 0 / 74%);
  191. transform: translateY(-100%);
  192. opacity: 0;
  193. }
  194.  
  195. .popup-enter {
  196. animation: slideDown 0.3s ease-in-out forwards;
  197. }
  198.  
  199. .popup-exit {
  200. animation: slideUp 0.3s ease-in-out forwards;
  201. }
  202.  
  203. @keyframes slideDown {
  204. from {
  205. transform: translateY(-100%);
  206. opacity: 0;
  207. }
  208. to {
  209. transform: translateY(0);
  210. opacity: 1;
  211. }
  212. }
  213.  
  214. @keyframes slideUp {
  215. from {
  216. transform: translateY(0);
  217. opacity: 1;
  218. }
  219. to {
  220. transform: translateY(-100%);
  221. opacity: 0;
  222. }
  223. }
  224.  
  225. .settings-popup .close-button {
  226. position: absolute;
  227. top: 8px;
  228. right: 8px;
  229. background: #2f213b;
  230. color: #fff;
  231. border: none;
  232. border-radius: 6px;
  233. padding: 6px 12px;
  234. cursor: pointer;
  235. font-size: 14px;
  236. transition: background 0.2s;
  237. }
  238.  
  239. .settings-popup .settings-title {
  240. margin: 0 0 12px 0;
  241. font-size: 18px;
  242. color: #2f213b;
  243. font-weight: bold;
  244. }
  245.  
  246. .settings-popup .settings-content {
  247. font-size: 14px;
  248. color: #fff;
  249. }
  250.  
  251. .settings-popup .settings-description {
  252. margin: 0 0 12px 0;
  253. color: #2f213b;
  254. }
  255.  
  256. .settings-popup .watchdog-section,
  257. .settings-popup .language-section,
  258. .settings-popup .font-size-section {
  259. margin-top: 15px;
  260. }
  261.  
  262. .settings-popup .watchdog-label,
  263. .settings-popup .language-label,
  264. .settings-popup .font-size-label {
  265. display: block;
  266. margin-bottom: 6px;
  267. font-size: 14px;
  268. color: #2f213b;
  269. }
  270.  
  271. .settings-popup .watchdog-interval,
  272. .settings-popup .language-select,
  273. .settings-popup .font-size-input {
  274. width: 125px;
  275. padding: 6px;
  276. border-radius: 6px;
  277. border: 1px solid #bda3d7;
  278. background: rgb(47, 33, 59);
  279. color: #bda3d7;
  280. font-size: 14px;
  281. }
  282.  
  283. .settings-popup .language-select {
  284. width: 125px;
  285. }
  286. `;
  287. document.head.appendChild(styleSheet);
  288.  
  289. // Функция для получения переводов
  290. function getTranslation(key, lang = GM_getValue('language', 'ru')) {
  291. const translations = {
  292. ru: {
  293. settingsFailed: 'Не удалось загрузить настройки',
  294. watchdogUpdated: 'Интервал watchdog обновлён до {interval}с',
  295. watchdogInvalid: 'Интервал должен быть минимум 1 секунда',
  296. languageUpdated: 'Язык интерфейса обновлён',
  297. closeButton: 'Закрыть',
  298. settingsTitle: 'Настройки',
  299. settingsDescription: 'Настройте параметры панели',
  300. watchdogLabel: 'Интервал проверки Watchdog (секунды):',
  301. languageLabel: 'Язык интерфейса:',
  302. blockedEmotesList: 'Список заблокированных эмодзи',
  303. platformLabel: 'Платформа',
  304. nameLabel: 'Имя',
  305. dateTimeLabel: 'Дата-Время',
  306. goToLastElement: 'Перейти к последнему элементу',
  307. showStatsChart: 'Показать статистику',
  308. deleteAll: 'Удалить все',
  309. export: 'Экспорт',
  310. import: 'Импорт',
  311. unblockAll: 'Разблокировать все эмодзи',
  312. blockAll: 'Заблокировать все эмодзи',
  313. searchPlaceholder: 'Поиск в списке заблокированных…',
  314. searchButton: 'Поиск',
  315. twitchChannelLabel: 'Twitch-канал',
  316. addChannelPlaceholder: 'Введите канал для добавления',
  317. addChannelButton: 'Добавить'
  318. },
  319. en: {
  320. settingsFailed: 'Failed to load settings',
  321. watchdogUpdated: 'Watchdog interval updated to {interval}s',
  322. watchdogInvalid: 'Interval must be at least 1 second',
  323. languageUpdated: 'Interface language updated',
  324. closeButton: 'Close',
  325. settingsTitle: 'Settings',
  326. settingsDescription: 'Configure panel settings',
  327. watchdogLabel: 'Watchdog check interval (seconds):',
  328. languageLabel: 'Interface language:',
  329. blockedEmotesList: 'List of Blocked Emotes',
  330. platformLabel: 'Platform',
  331. nameLabel: 'Name',
  332. dateTimeLabel: 'Date-Time',
  333. goToLastElement: 'Go to Last Element',
  334. showStatsChart: 'Show Stats Chart',
  335. deleteAll: 'Delete All',
  336. export: 'Export',
  337. import: 'Import',
  338. unblockAll: 'Unblock All Emotes',
  339. blockAll: 'Block All Emotes',
  340. searchPlaceholder: 'Search in blocked list…',
  341. searchButton: 'Search',
  342. twitchChannelLabel: 'Twitch Channel',
  343. addChannelPlaceholder: 'Type to add channel',
  344. addChannelButton: 'Add it'
  345. }
  346. };
  347. return translations[lang][key] || translations.ru[key];
  348. }
  349.  
  350. // Функция для обновления переводов панели
  351. function updatePanelTranslations(lang = GM_getValue('language', 'ru')) {
  352. const blockedEmotesList = document.querySelector('.blocked-emotes-list');
  353. if (blockedEmotesList) {
  354. blockedEmotesList.textContent = getTranslation('blockedEmotesList', lang);
  355. }
  356.  
  357. const platformLabel = document.querySelector('.platform-label');
  358. if (platformLabel) {
  359. platformLabel.textContent = getTranslation('platformLabel', lang);
  360. }
  361.  
  362. const nameLabel = document.querySelector('.name-label');
  363. if (nameLabel) {
  364. nameLabel.textContent = getTranslation('nameLabel', lang);
  365. }
  366.  
  367. const dateTimeLabel = document.querySelector('.date-time-label');
  368. if (dateTimeLabel) {
  369. dateTimeLabel.textContent = getTranslation('dateTimeLabel', lang);
  370. }
  371.  
  372. const goToLastElement = document.querySelector('.go-to-last-element');
  373. if (goToLastElement) {
  374. goToLastElement.textContent = getTranslation('goToLastElement', lang);
  375. }
  376.  
  377. const showStatsChart = document.querySelector('.show-stats-chart');
  378. if (showStatsChart) {
  379. showStatsChart.textContent = getTranslation('showStatsChart', lang);
  380. }
  381.  
  382. const deleteAll = document.querySelector('.delete-all');
  383. if (deleteAll) {
  384. deleteAll.textContent = getTranslation('deleteAll', lang);
  385. }
  386.  
  387. const exportButton = document.querySelector('.export-button');
  388. if (exportButton) {
  389. exportButton.textContent = getTranslation('export', lang);
  390. }
  391.  
  392. const importButton = document.querySelector('.import-button');
  393. if (importButton) {
  394. importButton.textContent = getTranslation('import', lang);
  395. }
  396.  
  397. const unblockAll = document.querySelector('.unblock-all');
  398. if (unblockAll) {
  399. unblockAll.textContent = getTranslation('unblockAll', lang);
  400. }
  401.  
  402. const blockAll = document.querySelector('.block-all');
  403. if (blockAll) {
  404. blockAll.textContent = getTranslation('blockAll', lang);
  405. }
  406.  
  407. const searchInput = document.querySelector('.search-input');
  408. if (searchInput) {
  409. searchInput.placeholder = getTranslation('searchPlaceholder', lang);
  410. }
  411.  
  412. const searchButton = document.querySelector('.search-button');
  413. if (searchButton) {
  414. searchButton.textContent = getTranslation('searchButton', lang);
  415. }
  416.  
  417. const twitchChannelLabel = document.querySelector('.twitch-channel-label');
  418. if (twitchChannelLabel) {
  419. twitchChannelLabel.textContent = getTranslation('twitchChannelLabel', lang);
  420. }
  421.  
  422. const addChannelInput = document.querySelector('.add-channel-input');
  423. if (addChannelInput) {
  424. addChannelInput.placeholder = getTranslation('addChannelPlaceholder', lang);
  425. }
  426.  
  427. const addChannelButton = document.querySelector('.add-channel-button');
  428. if (addChannelButton) {
  429. addChannelButton.textContent = getTranslation('addChannelButton', lang);
  430. }
  431.  
  432. settingsButton.title = getTranslation('settingsTitle', lang);
  433. }
  434.  
  435. // Инициализация переводов панели при загрузке
  436. const scriptVersion = '2.6.52';
  437. updatePanelTranslations();
  438.  
  439. // Обработчик клика по кнопке настроек
  440. settingsButton.onclick = () => {
  441. if (document.querySelector('.settings-popup')) {
  442. return;
  443. }
  444.  
  445. GM_xmlhttpRequest({
  446. method: 'GET',
  447. url: 'https://raw.githubusercontent.com/sopernik566/Control_Emotes_Panel_Twitch_JS/refs/heads/main/settingsPopup.html',
  448. onload: function(response) {
  449. try {
  450. const popup = document.createElement('div');
  451. popup.innerHTML = response.responseText;
  452. // Убедимся, что класс .settings-popup добавлен только один раз
  453. const popupContainer = popup.querySelector('.settings-popup') || popup;
  454. popupContainer.className = 'settings-popup';
  455. document.body.appendChild(popupContainer);
  456.  
  457. // Запускаем анимацию открытия
  458. requestAnimationFrame(() => {
  459. popupContainer.classList.add('popup-enter');
  460. });
  461.  
  462. const lang = GM_getValue('language', 'ru');
  463. const closeButton = popupContainer.querySelector('.close-button');
  464. if (closeButton) {
  465. closeButton.textContent = getTranslation('closeButton', lang);
  466. closeButton.onclick = () => {
  467. popupContainer.classList.remove('popup-enter');
  468. popupContainer.classList.add('popup-exit');
  469. popupContainer.addEventListener('animationend', () => {
  470. popupContainer.remove();
  471. }, { once: true });
  472. };
  473. closeButton.onmouseover = () => {
  474. closeButton.style.background = '#a88cc7';
  475. };
  476. closeButton.onmouseout = () => {
  477. closeButton.style.background = '#2f213b';
  478. };
  479. } else {
  480. console.error('[7BTTVFZ Control Emotes Panel] Кнопка закрытия не найдена');
  481. }
  482.  
  483. const settingsTitle = popupContainer.querySelector('.settings-title');
  484. if (settingsTitle) {
  485. settingsTitle.textContent = getTranslation('settingsTitle', lang);
  486. }
  487.  
  488. const settingsDescription = popupContainer.querySelector('.settings-description');
  489. if (settingsDescription) {
  490. settingsDescription.textContent = getTranslation('settingsDescription', lang);
  491. }
  492.  
  493. const watchdogLabel = popupContainer.querySelector('.watchdog-label');
  494. if (watchdogLabel) {
  495. watchdogLabel.textContent = getTranslation('watchdogLabel', lang);
  496. }
  497.  
  498. const languageLabel = popupContainer.querySelector('.language-label');
  499. if (languageLabel) {
  500. languageLabel.textContent = getTranslation('languageLabel', lang);
  501. }
  502.  
  503. const watchdogInput = popupContainer.querySelector('.watchdog-interval');
  504. if (watchdogInput) {
  505. watchdogInput.value = GM_getValue('watchdogInterval', 10);
  506. watchdogInput.onchange = () => {
  507. const interval = parseInt(watchdogInput.value, 10);
  508. if (interval >= 1) {
  509. GM_setValue('watchdogInterval', interval);
  510. console.log(`[7BTTVFZ Control Emotes Panel] Интервал watchdog установлен: ${interval} сек`);
  511. showNotification(getTranslation('watchdogUpdated', lang).replace('{interval}', interval), 3000);
  512. } else {
  513. showNotification(getTranslation('watchdogInvalid', lang), 3000);
  514. console.log('[7BTTVFZ Control Emotes Panel] Неверный интервал watchdog');
  515. }
  516. };
  517. }
  518.  
  519. const languageSelect = popupContainer.querySelector('.language-select');
  520. if (languageSelect) {
  521. languageSelect.value = GM_getValue('language', 'ru');
  522. languageSelect.onchange = () => {
  523. const newLang = languageSelect.value;
  524. GM_setValue('language', newLang);
  525. console.log(`[7BTTVFZ Control Emotes Panel] Язык интерфейса изменён на: ${newLang}`);
  526. showNotification(getTranslation('languageUpdated', newLang), 3000);
  527. if (closeButton) closeButton.textContent = getTranslation('closeButton', newLang);
  528. if (settingsTitle) settingsTitle.textContent = getTranslation('settingsTitle', newLang);
  529. if (settingsDescription) settingsDescription.textContent = getTranslation('settingsDescription', newLang);
  530. if (watchdogLabel) watchdogLabel.textContent = getTranslation('watchdogLabel', newLang);
  531. if (languageLabel) languageLabel.textContent = getTranslation('languageLabel', newLang);
  532. updatePanelTranslations(newLang);
  533. };
  534. }
  535.  
  536. // Поддержка настройки размера шрифта (если активирована)
  537. const fontSizeInput = popupContainer.querySelector('.font-size-input');
  538. if (fontSizeInput) {
  539. fontSizeInput.value = GM_getValue('buttonFontSize', 16);
  540. fontSizeInput.onchange = () => {
  541. const size = parseInt(fontSizeInput.value, 10);
  542. if (size >= 1 && size <= 35) {
  543. GM_setValue('buttonFontSize', size);
  544. console.log(`[7BTTVFZ Control Emotes Panel] Размер шрифта установлен: ${size}px`);
  545. showNotification(`Font size updated to ${size}px`, 3000);
  546. } else {
  547. showNotification('Font size must be between 1 and 35', 3000);
  548. console.log('[7BTTVFZ Control Emotes Panel] Неверный размер шрифта');
  549. }
  550. };
  551. }
  552.  
  553. console.log('[7BTTVFZ Control Emotes Panel] Попап настроек загружен с GitHub');
  554. } catch (err) {
  555. console.error('[7BTTVFZ Control Emotes Panel] Ошибка загрузки попапа:', err);
  556. showNotification(getTranslation('settingsFailed', lang), 3000);
  557. createFallbackSettingsPopup();
  558. }
  559. },
  560. onerror: function(err) {
  561. console.error('[7BTTVFZ Control Emotes Panel] Ошибка загрузки с GitHub:', err);
  562. showNotification(getTranslation('settingsFailed', GM_getValue('language', 'ru')), 3000);
  563. createFallbackSettingsPopup();
  564. }
  565. });
  566. };
  567.  
  568. // Резервный попап на случай сбоя
  569. function createFallbackSettingsPopup() {
  570. const settingsPopup = document.createElement('div');
  571. settingsPopup.className = 'settings-popup';
  572. const lang = GM_getValue('language', 'ru');
  573.  
  574. const closeButton = document.createElement('button');
  575. closeButton.className = 'close-button';
  576. closeButton.textContent = getTranslation('closeButton', lang);
  577. closeButton.onclick = () => {
  578. settingsPopup.classList.remove('popup-enter');
  579. settingsPopup.classList.add('popup-exit');
  580. settingsPopup.addEventListener('animationend', () => {
  581. settingsPopup.remove();
  582. }, { once: true });
  583. };
  584. closeButton.onmouseover = () => {
  585. closeButton.style.background = '#a88cc7';
  586. };
  587. closeButton.onmouseout = () => {
  588. closeButton.style.background = '#2f213b';
  589. };
  590.  
  591. settingsPopup.appendChild(closeButton);
  592. settingsPopup.innerHTML += `
  593. <h3 class="settings-title">${getTranslation('settingsTitle', lang)}</h3>
  594. <div class="settings-content">
  595. <p class="settings-description">${getTranslation('settingsFailed', lang)}</p>
  596. </div>
  597. `;
  598. document.body.appendChild(settingsPopup);
  599.  
  600. // Запускаем анимацию открытия
  601. requestAnimationFrame(() => {
  602. settingsPopup.classList.add('popup-enter');
  603. });
  604. }
  605. // ================ конец end of popupsettings ==================== //
  606.  
  607.  
  608.  
  609.  
  610. //---------------Текст title "Название" "лист" список list of BlockedEmotes ------------------------//
  611. const title = document.createElement('h4');
  612. title.innerText = 'list of BlockedEmotes';
  613. title.style.margin = '-5px 0px 10px'; // Отступы сверху и снизу
  614. title.style.color = ' #2a1e38'; // Обновленный цвет
  615. title.style.position = 'relative'; // Устанавливаем позицию относительно
  616. title.style.bottom = '35px'; // Сдвиг по вертикали
  617. title.style.width = '190px';
  618. controlPanel.appendChild(title);
  619.  
  620.  
  621.  
  622. //--------------- Список заблокированных каналов ------------------//
  623. const list = document.createElement('ul');
  624. list.id = 'blockedList';
  625. list.style.position = 'relative';
  626. list.style.bottom = '34px';
  627. list.style.border = '1px solid #ffffff'; // Белая граница
  628. list.style.borderRadius = '0px 0px 8px 8px'; // Скругление углов
  629. list.style.boxShadow = ' rgb(0 0 0 / 67%) -18px 69px 40px 0 inset '; // Вставка тени в контейнер
  630. list.style.listStyle = 'none'; // Убираем стандартные маркеры списка
  631. list.style.padding = '0'; // Убираем отступы
  632. list.style.margin = '-14px 0px 10px'; // Отступ снизу
  633. list.style.maxHeight = '570px'; // Устанавливаем максимальную высоту
  634. list.style.height = '410px'; // Высота списка
  635. list.style.overflowY = 'auto'; // Включаем вертикальную прокрутку
  636.  
  637.  
  638. //==================================== ГРАДИЕНТ ФОН СПИСОК =================================================//
  639. // Добавляем линейный градиент фона с кроссбраузерностью
  640. list.style.background = 'linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)';
  641. list.style.background = '-moz-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)'; // Для Firefox
  642. list.style.background = '-webkit-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)'; // Для Safari и Chrome
  643. list.style.filter = 'progid: DXImageTransform.Microsoft.gradient(startColorstr="#36173b", endColorstr="#589F97", GradientType=1)'; // Для старых версий IE
  644. list.style.color = '#fff'; // Белый цвет текста
  645.  
  646. //========== кастомный scroll bar для списка =============//
  647. const style = document.createElement('style');
  648. style.innerHTML = `
  649. #blockedList::-webkit-scrollbar {
  650. width: 25px; /* Ширина скроллбара */
  651. }
  652.  
  653. #blockedList::-webkit-scrollbar-thumb {
  654. background-color: #C1A5EF; /* Цвет бегунка */
  655. border-radius: 8px; /* Скругление бегунка */
  656. border: 3px solid #4F3E6A; /* Внутренний отступ (цвет трека) */
  657. height: 80px; /* Высота бегунка */
  658. }
  659.  
  660. #blockedList::-webkit-scrollbar-thumb:hover {
  661. background-color: #C6AEFF; /* Цвет бегунка при наведении */
  662. }
  663.  
  664. #blockedList::-webkit-scrollbar-thumb:active {
  665. background-color: #B097C9; /* Цвет бегунка при активном состоянии */
  666. }
  667.  
  668. #blockedList::-webkit-scrollbar-track {
  669. background: #455565; /* Цвет трека */
  670. border-radius: 0px 0px 8px 0px; /* Скругление только нижнего правого угла */
  671. }
  672.  
  673. #blockedList::-webkit-scrollbar-track:hover {
  674. background-color: #455565; /* Цвет трека при наведении */
  675. }
  676.  
  677. #blockedList::-webkit-scrollbar-track:active {
  678. background-color: #455565; /* Цвет трека при активном состоянии */
  679. }
  680.  
  681. `;
  682. document.head.appendChild(style);
  683.  
  684. // hover blocked-item элемент списка //
  685. const hoverStyle = document.createElement('style');
  686. hoverStyle.innerHTML = `
  687. .blocked-item {
  688. transition: background-color 0.3s ease, color 0.3s ease;
  689. }
  690. .blocked-item:hover {
  691. background-color: rgba(167, 54, 54, 0.52);
  692. color: #42d13a;
  693. }
  694. .blocked-item:hover span {
  695. color: #42d13a;
  696. }
  697. .new-item {
  698. background-color:#28a828;
  699. transition: background-color 0.3s ease;
  700. }
  701. .new-item:hover {
  702. background-color: #3a2252;
  703. color: #af7fcf;
  704. }
  705. .new-item:hover span {
  706. color: #af7fcf;
  707. }
  708. #sortContainer button {
  709. background: none;
  710. border: none;
  711. color: inherit;
  712. font-family: inherit;
  713. font-size: inherit;
  714. padding: 0 10px;
  715. transition: color 0.3s ease;
  716. }
  717. #sortContainer button:hover {
  718. color: #9ae048;
  719. }
  720. `;
  721. document.head.appendChild(hoverStyle);
  722.  
  723.  
  724.  
  725. const highlightStyle = document.createElement('style');
  726. highlightStyle.innerHTML = `
  727. .blocked-item .highlight {
  728. background-color: #FFEB3B !important; /* Красный фон для подсветки */
  729. color:rgb(0, 0, 0) !important; /* Белый текст для контраста */
  730. padding: 0 2px !important;
  731. border-radius: 2px !important;
  732. transition: background-color 0.5s ease !important;
  733. }
  734. .blocked-item.highlight-item {
  735. background-color: rgba(163, 161, 18, 0.83) !important; /* Полупрозрачная красная подсветка для всего элемента */
  736. transition: background-color 0.5s ease !important;
  737. }
  738. .last-item-highlight {
  739. background-color: #279479; /* Полупрозрачный золотой фон */
  740. transition: background-color 0.5s ease; /* Плавное исчезновение */
  741. }
  742. `;
  743. document.head.appendChild(highlightStyle);
  744.  
  745. document.head.appendChild(style);
  746.  
  747. const buttonColor = ' #907cad'; // Общий цвет для кнопок
  748. const buttonShadow = '0 4px 8px rgba(0, 0, 0, 0.6)'; // Тень для кнопок (60% прозрачности)
  749.  
  750.  
  751.  
  752. // Функция для обновления списка заблокированных каналов
  753.  
  754. // Переменные для хранения ID заблокированных элементов
  755. let blockedEmoteIDs = new Set();
  756. let blockedChannelIDs = new Set();
  757. let newlyAddedIds = new Set();
  758.  
  759. function updateBlockedList() {
  760. list.innerHTML = '';
  761.  
  762. // Очистка и обновление Set для быстрого поиска ID
  763. blockedEmoteIDs.clear();
  764. blockedChannelIDs.clear();
  765.  
  766. function createListItem(channel, isNew = false) {
  767. const item = document.createElement('li');
  768. item.className = 'blocked-item';
  769. item.dataset.id = channel.id;
  770.  
  771. if (isNew) {
  772. item.classList.add('new-item');
  773. setTimeout(() => {
  774. item.classList.remove('new-item');
  775. }, 1800000);
  776. }
  777.  
  778. item.style.display = 'flex';
  779. item.style.flexDirection = 'column';
  780. item.style.padding = '5px';
  781. item.style.borderBottom = '1px solid #eee';
  782.  
  783. const topRow = document.createElement('div');
  784. topRow.style.display = 'flex';
  785. topRow.style.justifyContent = 'space-between';
  786. topRow.style.alignItems = 'center';
  787.  
  788. const channelName = document.createElement('span');
  789. if (channel.platform === 'TwitchChannel') {
  790. channelName.innerText = `${channel.platform} > name emote: ${channel.emoteName}`;
  791. } else {
  792. channelName.innerText = `${channel.platform} > ${channel.emoteName}`;
  793. }
  794. channelName.classList.add('list-item-text');
  795. channelName.style.flex = '1';
  796. channelName.style.fontSize = '14px';
  797. channelName.style.fontWeight = 'bold';
  798. channelName.style.whiteSpace = 'nowrap';
  799. channelName.style.overflow = 'hidden';
  800. channelName.style.textOverflow = 'ellipsis';
  801. topRow.appendChild(channelName);
  802.  
  803. const dateInfo = document.createElement('span');
  804. const date = new Date(channel.date);
  805. dateInfo.innerText = isNaN(date.getTime())
  806. ? 'Unknown Date'
  807. : date.toLocaleString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
  808. dateInfo.classList.add('list-item-date');
  809. dateInfo.style.marginRight = '30px';
  810. dateInfo.style.fontSize = '14px';
  811. topRow.appendChild(dateInfo);
  812.  
  813. const removeButton = document.createElement('button');
  814. removeButton.innerText = 'Delete';
  815. removeButton.classList.add('delete-button');
  816. console.log("[7BTTVFZ Control Emotes Panel] Создана кнопка Delete для элемента:", channel.id);
  817.  
  818. // Используем сохранённые стили из currentDeleteButtonStyles
  819. removeButton.style.background = currentDeleteButtonStyles.background;
  820. removeButton.style.color = currentDeleteButtonStyles.color;
  821. removeButton.style.height = '35px';
  822. removeButton.style.width = '75px';
  823. removeButton.style.fontWeight = 'bold';
  824. removeButton.style.fontSize = '16px';
  825. removeButton.style.border = 'none';
  826. removeButton.style.borderRadius = '4px';
  827. removeButton.style.cursor = 'pointer';
  828. removeButton.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.6)';
  829. removeButton.style.display = 'flex';
  830. removeButton.style.alignItems = 'center';
  831. removeButton.style.justifyContent = 'center';
  832. removeButton.style.transition = 'background 0.3s ease';
  833. removeButton.style.opacity = '1';
  834. removeButton.style.visibility = 'visible';
  835.  
  836. // Добавляем обработчики наведения
  837. removeButton.onmouseover = () => {
  838. removeButton.style.background = currentDeleteButtonStyles.hoverBackground;
  839. };
  840. removeButton.onmouseout = () => {
  841. removeButton.style.background = currentDeleteButtonStyles.background;
  842. };
  843.  
  844. removeButton.onclick = function () {
  845. if (channel.platform === 'TwitchChannel') {
  846. blockedChannels = blockedChannels.filter(c => c.id !== channel.id);
  847. blockedChannelIDs.delete(channel.id);
  848. GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2));
  849. } else {
  850. blockedEmotes = blockedEmotes.filter(c => c.id !== channel.id);
  851. blockedEmoteIDs.delete(channel.id);
  852. GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2));
  853. }
  854.  
  855. newlyAddedIds.delete(channel.id);
  856. updateBlockedList();
  857. updateCounter();
  858. showEmoteForChannel(channel);
  859. };
  860.  
  861. topRow.appendChild(removeButton);
  862. item.appendChild(topRow);
  863.  
  864. const channelLink = document.createElement('span');
  865. channelLink.innerText = `(prefix: ${channel.name})`;
  866. channelLink.classList.add('list-item-link');
  867. channelLink.style.fontSize = '14px';
  868. channelLink.style.wordBreak = 'break-word';
  869. channelLink.style.marginTop = '1px';
  870. item.appendChild(channelLink);
  871.  
  872. return item;
  873. }
  874.  
  875. // Заполняем списки и обновляем Set
  876. blockedChannels.forEach(channel => {
  877. blockedChannelIDs.add(channel.id);
  878. const isNew = newlyAddedIds.has(channel.id) && Array.from(newlyAddedIds).pop() === channel.id;
  879. list.appendChild(createListItem(channel, isNew));
  880. });
  881.  
  882. blockedEmotes.forEach(channel => {
  883. blockedEmoteIDs.add(channel.id);
  884. const isNew = newlyAddedIds.has(channel.id) && Array.from(newlyAddedIds).pop() === channel.id;
  885. list.appendChild(createListItem(channel, isNew));
  886. });
  887.  
  888. // Прокручиваем к последнему добавленному элементу внутри контейнера list
  889. if (newlyAddedIds.size > 0) {
  890. const lastAddedId = Array.from(newlyAddedIds).pop();
  891. const newItem = list.querySelector(`[data-id="${lastAddedId}"]`);
  892. if (newItem) {
  893. // Вычисляем позицию нового элемента относительно контейнера list
  894. const itemOffsetTop = newItem.offsetTop; // Позиция элемента относительно начала списка
  895. const listHeight = list.clientHeight; // Видимая высота контейнера list
  896. const itemHeight = newItem.clientHeight; // Высота самого элемента
  897.  
  898. // Вычисляем, куда нужно прокрутить, чтобы элемент оказался вверху видимой области
  899. const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2);
  900.  
  901. // Плавно прокручиваем list к нужной позиции
  902. list.scrollTo({
  903. top: scrollPosition,
  904. behavior: 'smooth'
  905. });
  906. }
  907. }
  908.  
  909. // Очищаем список новых ID после отображения
  910. newlyAddedIds.clear();
  911. }
  912.  
  913. // Добавляем список в панель управления
  914. controlPanel.appendChild(list);
  915.  
  916.  
  917.  
  918.  
  919. // Создаём контейнер для поисковой строки
  920. const searchContainer = document.createElement('div');
  921. searchContainer.style.display = 'flex';
  922. searchContainer.style.gap = '5px';
  923. searchContainer.style.top = '500px';
  924. searchContainer.style.position = 'relative';
  925.  
  926. // Создаём поисковую строку
  927. const searchInput = document.createElement('input');
  928. searchInput.type = 'text';
  929. searchInput.placeholder = 'Search in blocked list...';
  930. searchInput.style.background = ' #192427';
  931. searchInput.style.width = '459px';
  932. searchInput.style.left = '132px';
  933. searchInput.style.color = ' #b69dcf';
  934. searchInput.style.fontWeight = 'bold';
  935. searchInput.style.height = '35px';
  936. searchInput.style.padding = '5px';
  937. searchInput.style.border = '1px solid #b69dcf';
  938. searchInput.style.borderRadius = '4px';
  939. searchInput.style.boxShadow = ' #4c2a5e 0px 4px 6px inset';
  940. searchInput.style.position = 'relative';
  941. searchInput.style.bottom = '50px';
  942.  
  943. // Создаём кнопку поиска
  944. const searchButton = document.createElement('button');
  945. searchButton.innerText = 'Search'; // Меняем текст на "Search"
  946. searchButton.style.background = buttonColor;
  947. searchButton.style.position = 'relative';
  948. searchButton.style.bottom = '50px';
  949. searchButton.style.color = '#fff';
  950. searchButton.style.border = 'none';
  951. searchButton.style.width = '72px';
  952. searchButton.style.left = '132px';
  953. searchButton.style.borderRadius = '4px';
  954. searchButton.style.padding = '5px 10px';
  955. searchButton.style.cursor = 'pointer';
  956. searchButton.style.fontSize = '16px';
  957. searchButton.style.fontWeight = 'bold';
  958. searchButton.style.boxShadow = buttonShadow;
  959.  
  960. // Добавляем ховер-эффекты для кнопки поиска
  961. searchButton.onmouseover = function() {
  962. searchButton.style.background = '-webkit-linear-gradient(135deg, #443157 0%,rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)';
  963. };
  964. searchButton.onmouseout = function() {
  965. searchButton.style.background = buttonColor;
  966. };
  967.  
  968. // Обработчик кнопки поиска
  969. searchButton.onclick = () => {
  970. const searchTerm = searchInput.value.trim();
  971. filterBlockedList(searchTerm); // Запускаем фильтрацию
  972. };
  973.  
  974. function filterBlockedList(searchTerm) {
  975. const lowerSearchTerm = searchTerm.toLowerCase().trim();
  976. console.log("[7BTTVFZ Control Emotes Panel] Поисковый запрос (lowerSearchTerm):", lowerSearchTerm);
  977.  
  978. let filteredList = [];
  979.  
  980. // Фильтрация списка
  981. if (!lowerSearchTerm) {
  982. filteredList = [...blockedChannels, ...blockedEmotes];
  983. console.log("[7BTTVFZ Control Emotes Panel] Поиск пустой, отображаем все элементы:", filteredList);
  984. } else {
  985. filteredList = [...blockedChannels, ...blockedEmotes].filter(item => {
  986. const emoteName = item.emoteName || '';
  987. const platform = item.platform || '';
  988. const name = item.name || '';
  989. const matches =
  990. emoteName.toLowerCase().includes(lowerSearchTerm) ||
  991. platform.toLowerCase().includes(lowerSearchTerm) ||
  992. name.toLowerCase().includes(lowerSearchTerm);
  993. console.log(`[7BTTVFZ Control Emotes Panel] Проверяем элемент: ${JSON.stringify(item)}, совпадение: ${matches}`);
  994. return matches;
  995. });
  996. console.log("[7BTTVFZ Control Emotes Panel] Результаты фильтрации:", filteredList);
  997. }
  998.  
  999. // Сохраняем текущую позицию скролла
  1000. const currentScrollPosition = list.scrollTop;
  1001. console.log("[7BTTVFZ Control Emotes Panel] Текущая позиция скролла перед обновлением:", currentScrollPosition);
  1002.  
  1003. // Получаем текущие элементы в DOM
  1004. const currentItems = list.querySelectorAll('.blocked-item');
  1005. const existingIds = new Set([...currentItems].map(item => item.dataset.id));
  1006.  
  1007. // Удаляем элементы, которые не прошли фильтрацию
  1008. currentItems.forEach(item => {
  1009. const itemId = item.dataset.id;
  1010. if (!filteredList.some(f => f.id === itemId)) {
  1011. item.remove();
  1012. }
  1013. });
  1014.  
  1015. // Добавляем или обновляем элементы
  1016. filteredList.forEach(channel => {
  1017. const itemId = channel.id;
  1018. let item = list.querySelector(`[data-id="${itemId}"]`);
  1019.  
  1020. if (!item) {
  1021. // Если элемента нет, создаём новый
  1022. item = createListItem(channel);
  1023. list.appendChild(item);
  1024. }
  1025.  
  1026. // Применяем подсветку, если есть поисковый запрос
  1027. if (lowerSearchTerm) {
  1028. const spans = item.querySelectorAll('span');
  1029. spans.forEach(span => {
  1030. const originalText = span.textContent || '';
  1031. if (originalText.toLowerCase().includes(lowerSearchTerm)) {
  1032. const regex = new RegExp(`(${lowerSearchTerm})`, 'gi');
  1033. const highlightedText = originalText.replace(regex, '<span class="highlight">$1</span>');
  1034. span.innerHTML = highlightedText;
  1035. }
  1036. });
  1037. }
  1038. });
  1039.  
  1040. // Прокрутка к первому элементу
  1041. if (filteredList.length > 0) {
  1042. const firstItem = list.querySelector('.blocked-item');
  1043. if (firstItem) {
  1044. console.log("[7BTTVFZ Control Emotes Panel] Найден первый элемент для прокрутки:", firstItem);
  1045. const firstItemOffset = firstItem.offsetTop;
  1046. list.scrollTop = firstItemOffset - (list.clientHeight / 2) + (firstItem.clientHeight / 2);
  1047. console.log("[7BTTVFZ Control Emotes Panel] Установлен scrollTop:", list.scrollTop);
  1048. } else {
  1049. console.log("[7BTTVFZ Control Emotes Panel] Первый элемент не найден в DOM!");
  1050. }
  1051. } else {
  1052. // Если список пуст, восстанавливаем скролл
  1053. console.log("[7BTTVFZ Control Emotes Panel] Список пуст, восстанавливаем скролл на:", currentScrollPosition);
  1054. list.scrollTop = currentScrollPosition;
  1055. }
  1056.  
  1057. updateCounter();
  1058. }
  1059.  
  1060. // Добавляем элементы в контейнер поиска
  1061. searchContainer.appendChild(searchInput);
  1062. searchContainer.appendChild(searchButton); // searchButton
  1063.  
  1064.  
  1065. // Добавляем контейнер поиска в панель управления
  1066. controlPanel.appendChild(searchContainer);
  1067.  
  1068. // Далее продолжаем с добавлением списка
  1069. controlPanel.appendChild(list);
  1070.  
  1071.  
  1072. //================= Функционал для добавления нового канала в список заблокированных ==================//
  1073. const inputContainer = document.createElement('div');
  1074. inputContainer.style.display = 'flex';
  1075. inputContainer.style.gap = '5px';
  1076.  
  1077. const input = document.createElement('input');
  1078. input.type = 'text';
  1079. input.placeholder = 'type to add channel ';
  1080. input.style.position = 'relative';
  1081. input.style.background = ' #192427';
  1082. input.style.color = ' #b69dcf';
  1083. input.style.flex = '1';
  1084. input.style.fontWeight = 'bold'; // Жирный текст
  1085. input.style.height = '35px'; // Отступ между кнопкой и поисковой строкой
  1086. input.style.padding = '5px';
  1087. input.style.border = '1px solid #b69dcf';
  1088. input.style.borderRadius = '4px';
  1089. input.style.top = '15px'; // Отступ между кнопкой и поисковой строкой
  1090. // Добавление тени с фиолетовым цветом (35% прозрачности) внутрь
  1091. input.style.boxShadow = ' #4c2a5e 0px 4px 6px inset'; // Тень фиолетового цвета внутри
  1092.  
  1093. //================== Add it Button =====================//
  1094. // ==================== Кнопка добавления ===================== //
  1095. const addButton = document.createElement('button');
  1096. addButton.innerText = 'Add it';
  1097. addButton.style.background = buttonColor;
  1098. addButton.style.top = '15px'; // Отступ между кнопкой и поисковой строкой
  1099. addButton.style.position = 'relative';
  1100. addButton.style.color = '#fff';
  1101. addButton.style.border = 'none';
  1102. addButton.style.width = '72px';
  1103. addButton.style.borderRadius = '4px';
  1104. addButton.style.padding = '5px 10px';
  1105. addButton.style.cursor = 'pointer';
  1106. addButton.style.boxShadow = buttonShadow; // Тень для кнопки "Add it"
  1107.  
  1108. // Увеличиваем размер текста и делаем его жирным
  1109. addButton.style.fontSize = '16px'; // Увеличиваем размер текста
  1110. addButton.style.fontWeight = 'bold'; // Жирный текст
  1111.  
  1112. // Генерация уникального ID
  1113. function generateID() {
  1114. return `emote_${Date.now()}`; // Генерация ID на основе времени
  1115. }
  1116.  
  1117. addButton.onclick = (event) => {
  1118. event.preventDefault();
  1119. const channel = input.value.trim();
  1120. const platform = platformSelect.value;
  1121.  
  1122. if (channel) {
  1123. let emoteName = channel;
  1124. let emoteUrl = channel;
  1125. const emoteId = generateRandomID();
  1126.  
  1127. // Проверка на дублирование
  1128. const isDuplicate = platform === 'TwitchChannel'
  1129. ? blockedChannels.some(e => e.name === channel && e.platform === platform)
  1130. : blockedEmotes.some(e => e.emoteUrl === channel && e.platform === platform);
  1131.  
  1132. if (isDuplicate) {
  1133. console.log(`%c[7BTTVFZ Control Emotes Panel] %cChannel/Emote already blocked: ${channel}`,
  1134. 'color: rgb(255, 165, 0); font-weight: bold;',
  1135. 'color: rgb(255, 165, 0);');
  1136. return;
  1137. }
  1138.  
  1139. if (platform === '7tv' || platform === 'bttTV' || platform === 'ffz') {
  1140. const img = document.querySelector(`img[src="${channel}"]`);
  1141. if (img) {
  1142. emoteName = img.alt || channel.split('/').pop();
  1143. emoteUrl = img.src || channel;
  1144. }
  1145.  
  1146. const newEmote = {
  1147. id: emoteId,
  1148. name: emoteUrl,
  1149. platform: platform,
  1150. emoteName: emoteName,
  1151. emoteUrl: emoteUrl,
  1152. date: new Date().toISOString()
  1153. };
  1154.  
  1155. blockedEmotes.push(newEmote);
  1156. blockedEmoteIDs.add(emoteId);
  1157. newlyAddedIds.add(emoteId);
  1158. GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2));
  1159. console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedEmotes:`,
  1160. 'color: rgb(0, 255, 0); font-weight: bold;',
  1161. 'color: rgb(0, 255, 0);', newEmote);
  1162. } else if (platform === 'TwitchChannel') {
  1163. const prefix = channel.split(/[^a-zA-Z0-9]/)[0];
  1164. emoteUrl = prefix;
  1165.  
  1166. const newChannel = {
  1167. id: emoteId,
  1168. name: emoteUrl,
  1169. platform: platform,
  1170. emoteName: emoteName,
  1171. emoteUrl: emoteUrl,
  1172. date: new Date().toISOString()
  1173. };
  1174.  
  1175. blockedChannels.push(newChannel);
  1176. blockedChannelIDs.add(emoteId);
  1177. newlyAddedIds.add(emoteId);
  1178. GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2));
  1179. console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedChannels:`,
  1180. 'color: rgb(0, 255, 0); font-weight: bold;',
  1181. 'color: rgb(0, 255, 0);', newChannel);
  1182. }
  1183.  
  1184. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1185. if (chatContainer) {
  1186. toggleEmotesInNode(chatContainer);
  1187. }
  1188.  
  1189. updateBlockedList();
  1190. updateCounter();
  1191. input.value = '';
  1192. }
  1193. };
  1194.  
  1195.  
  1196.  
  1197. // ==================== Создание выпадающего списка платформ ===================== //
  1198. const platformSelect = document.createElement('select');
  1199. platformSelect.style.top = '15px'; // Отступ между кнопкой и поисковой строкой
  1200. platformSelect.style.position = 'relative';
  1201. platformSelect.style.height = '35px'; // Высота выпадающего списка
  1202. platformSelect.style.border = '1px solid #c1a5ef';
  1203. platformSelect.style.background = '#192427';
  1204. platformSelect.style.borderRadius = '4px';
  1205. platformSelect.style.padding = '5px';
  1206. platformSelect.style.fontWeight = 'bold'; // Жирный текст
  1207. platformSelect.style.color = ' #b69dcf';
  1208.  
  1209. const platforms = ['TwitchChannel', '7tv', 'bttTV', 'ffz'];
  1210. platforms.forEach(platform => {
  1211. const option = document.createElement('option');
  1212. option.value = platform;
  1213. option.innerText = platform;
  1214. platformSelect.appendChild(option);
  1215. });
  1216.  
  1217. // ==================== Подсказки для выбора платформы ===================== //
  1218. platformSelect.addEventListener('change', () => {
  1219. const placeholderText = {
  1220. 'TwitchChannel': 'example prefix abcd123',
  1221. '7tv': 'link example: https://cdn.7tv.app/emote/00000000000000000000000000/2x.webp',
  1222. 'bttTV': 'link example: https://cdn.betterttv.net/emote/000000000000000000000000/2x.webp',
  1223. 'ffz': 'link example: https://cdn.frankerfacez.com/emote/0000/2'
  1224. };
  1225. input.placeholder = placeholderText[platformSelect.value];
  1226. });
  1227.  
  1228. // ==================== Добавление выпадающего списка в контейнер ===================== //
  1229. inputContainer.appendChild(platformSelect);
  1230.  
  1231.  
  1232.  
  1233. //----------------Единый контейнер для кнопок -------------------------//
  1234. const buttonContainer = document.createElement('div');
  1235. buttonContainer.style.display = 'flex'; // Используем flexbox для расположения кнопок в строку
  1236. buttonContainer.style.alignItems = 'baseline'; // Используем flexbox для расположения кнопок в строку
  1237. buttonContainer.style.alignContent = 'stretch';
  1238.  
  1239.  
  1240.  
  1241. buttonContainer.style.gap = '13px'; // Задаем промежуток между кнопками
  1242. buttonContainer.style.bottom = '113px'; // Отступ сверху для контейнера кнопок
  1243. buttonContainer.style.position = 'relative'; // Позиционирование относительно
  1244. buttonContainer.style.fontWeight = 'bold'; // Жирный текст для контейнера кнопок
  1245. buttonContainer.style.fontSize = '16px'; // Размер шрифта для кнопок
  1246. buttonContainer.style.width = '668px'; // Ширина кнопок (увеличена для эффекта растяжения
  1247.  
  1248.  
  1249.  
  1250.  
  1251. //-------------- Кнопка "Delete all" ------------------------//
  1252. const clearAllButton = document.createElement('button');
  1253. clearAllButton.innerText = 'Delete all'; // Текст на кнопке
  1254. clearAllButton.style.background = buttonColor; // Цвет фона кнопки
  1255. clearAllButton.style.color = '#fff'; // Цвет текста кнопки
  1256. clearAllButton.style.border = 'none'; // Убираем бордер у кнопки
  1257. clearAllButton.style.borderRadius = '4px'; // Скругленные углы кнопки
  1258. clearAllButton.style.padding = '5px 10px'; // Отступы внутри кнопки
  1259. clearAllButton.style.cursor = 'pointer'; // Курсор в виде руки при наведении
  1260. clearAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Delete all"
  1261.  
  1262. buttonContainer.appendChild(clearAllButton); // Добавляем кнопку в контейнер
  1263.  
  1264. // Обработчик события для кнопки "Delete all"
  1265. clearAllButton.onclick = () => {
  1266. blockedEmotes = [];
  1267. blockedChannels = [];
  1268. GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2));
  1269. GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2));
  1270. console.log("[7BTTVFZ Control Emotes Panel] Очищены blockedEmotes и blockedChannels");
  1271. updateBlockedList();
  1272. updateCounter();
  1273. };
  1274.  
  1275.  
  1276. //----------------- export Button --------------------//
  1277. const exportButton = document.createElement('button');
  1278. exportButton.innerText = 'Export';
  1279. exportButton.style.background = buttonColor;
  1280. exportButton.style.color = '#fff';
  1281. exportButton.style.border = 'none';
  1282. exportButton.style.borderRadius = '4px';
  1283. exportButton.style.padding = '5px 10px';
  1284. exportButton.style.cursor = 'pointer';
  1285. exportButton.style.boxShadow = buttonShadow; // Тень для кнопки "Export"
  1286. buttonContainer.appendChild(exportButton);
  1287. exportButton.onclick = () => {
  1288. const combinedData = {
  1289. blockedEmotes: blockedEmotes,
  1290. blockedChannels: blockedChannels
  1291. };
  1292. const blob = new Blob([JSON.stringify(combinedData, null, 2)], { type: 'application/json' });
  1293. const url = URL.createObjectURL(blob);
  1294. const link = document.createElement('a');
  1295. link.href = url;
  1296. link.download = 'blocked_data.json';
  1297. link.click();
  1298. URL.revokeObjectURL(url);
  1299. console.log("[7BTTVFZ Control Emotes Panel] Экспортированы данные:", combinedData);
  1300. };
  1301.  
  1302.  
  1303. //================= importButton ========================//
  1304. // Перемещаем создание fileInput в глобальную область, чтобы избежать дублирования
  1305. let fileInput = null;
  1306.  
  1307. // Функция для создания кнопки "Import"
  1308. function createImportButton() {
  1309. const button = document.createElement('button');
  1310. button.innerText = 'Import';
  1311. button.style.background = buttonColor;
  1312. button.style.color = '#fff';
  1313. button.style.border = 'none';
  1314. button.style.borderRadius = '4px';
  1315. button.style.padding = '5px 10px';
  1316. button.style.cursor = 'pointer';
  1317. button.style.boxShadow = buttonShadow;
  1318. return button;
  1319. }
  1320.  
  1321. // Функция для создания или переиспользования элемента input типа "file"
  1322. function createFileInput() {
  1323. if (!fileInput) {
  1324. fileInput = document.createElement('input');
  1325. fileInput.type = 'file';
  1326. fileInput.accept = 'application/json';
  1327. fileInput.style.display = 'none';
  1328. fileInput.onchange = handleFileChange;
  1329. document.body.appendChild(fileInput);
  1330. }
  1331. return fileInput;
  1332. }
  1333.  
  1334. // Инициализация кнопки "Import"
  1335. const importButton = createImportButton();
  1336. buttonContainer.appendChild(importButton);
  1337.  
  1338. importButton.onclick = () => {
  1339. const input = createFileInput();
  1340. input.value = ''; // Сбрасываем значение для повторного выбора файла
  1341. input.click();
  1342. };
  1343.  
  1344. // Обработка изменений файла
  1345. function handleFileChange(event) {
  1346. const file = event.target.files[0];
  1347. if (!file) {
  1348. console.log("[7BTTVFZ Control Emotes Panel] Файл не выбран");
  1349. return;
  1350. }
  1351. const reader = new FileReader();
  1352. reader.onload = handleFileLoad;
  1353. reader.onerror = () => {
  1354. console.error("[7BTTVFZ Control Emotes Panel] Ошибка чтения файла");
  1355. alert('Ошибка при чтении файла!');
  1356. };
  1357. reader.readAsText(file);
  1358. }
  1359.  
  1360. // Обработка загрузки файла
  1361. function handleFileLoad(event) {
  1362. try {
  1363. const importedData = JSON.parse(event.target.result);
  1364.  
  1365. if (!importedData || (!importedData.blockedEmotes && !importedData.blockedChannels)) {
  1366. throw new Error('Неверный формат файла! Ожидается объект с blockedEmotes и/или blockedChannels');
  1367. }
  1368.  
  1369. processImportedData(importedData);
  1370. updateInterface();
  1371. console.log("[7BTTVFZ Control Emotes Panel] Импорт успешно завершен");
  1372. } catch (err) {
  1373. console.error('[7BTTVFZ Control Emotes Panel] Ошибка при парсинге файла:', err);
  1374. alert(`Ошибка импорта: ${err.message}`);
  1375. }
  1376. }
  1377.  
  1378. // Обработка импортированных данных
  1379. function processImportedData(importedData) {
  1380. blockedEmotes = [];
  1381. blockedChannels = [];
  1382. blockedEmoteIDs.clear();
  1383. blockedChannelIDs.clear();
  1384. newlyAddedIds.clear();
  1385.  
  1386. if (Array.isArray(importedData.blockedEmotes)) {
  1387. importedData.blockedEmotes.forEach(emote => {
  1388. const newId = emote.id && !blockedEmoteIDs.has(emote.id) && !blockedChannelIDs.has(emote.id)
  1389. ? emote.id
  1390. : generateRandomID();
  1391.  
  1392. const newEmote = {
  1393. id: newId,
  1394. name: emote.name || emote.emoteUrl || '',
  1395. platform: emote.platform || 'unknown',
  1396. emoteName: emote.emoteName || getDefaultEmoteName(emote),
  1397. emoteUrl: emote.emoteUrl || emote.name || '',
  1398. date: emote.date || new Date().toISOString()
  1399. };
  1400.  
  1401. blockedEmotes.push(newEmote);
  1402. blockedEmoteIDs.add(newId);
  1403. newlyAddedIds.add(newId);
  1404. });
  1405. }
  1406.  
  1407. if (Array.isArray(importedData.blockedChannels)) {
  1408. importedData.blockedChannels.forEach(channel => {
  1409. const newId = channel.id && !blockedChannelIDs.has(channel.id) && !blockedEmoteIDs.has(channel.id)
  1410. ? channel.id
  1411. : generateRandomID();
  1412.  
  1413. const newChannel = {
  1414. id: newId,
  1415. name: channel.name || channel.emoteUrl || '',
  1416. platform: channel.platform || 'TwitchChannel',
  1417. emoteName: channel.emoteName || getDefaultEmoteName(channel),
  1418. emoteUrl: channel.emoteUrl || channel.name || '',
  1419. date: channel.date || new Date().toISOString()
  1420. };
  1421.  
  1422. blockedChannels.push(newChannel);
  1423. blockedChannelIDs.add(newId);
  1424. newlyAddedIds.add(newId);
  1425. });
  1426. }
  1427.  
  1428. GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2));
  1429. GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2));
  1430. console.log("[7BTTVFZ Control Emotes Panel] Импортированы blockedEmotes:", blockedEmotes);
  1431. console.log("[7BTTVFZ Control Emotes Panel] Импортированы blockedChannels:", blockedChannels);
  1432. }
  1433.  
  1434. // Функция обновления интерфейса
  1435. function updateInterface() {
  1436. blockedEmotes = loadData("blockedEmotes", []);
  1437. blockedChannels = loadData("blockedChannels", []);
  1438.  
  1439. blockedEmoteIDs.clear();
  1440. blockedChannelIDs.clear();
  1441. blockedEmotes.forEach(emote => blockedEmoteIDs.add(emote.id));
  1442. blockedChannels.forEach(channel => blockedChannelIDs.add(channel.id));
  1443.  
  1444. updateBlockedList();
  1445. updateCounter();
  1446.  
  1447. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1448. if (chatContainer) {
  1449. toggleEmotesInNode(chatContainer); // Используем toggleEmotesInNode вместо hideEmotesForChannel
  1450. } else {
  1451. console.log(
  1452. "%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден при обновлении интерфейса",
  1453. 'color:rgb(218, 93, 9); font-weight: bold;',
  1454. 'color: rgb(218, 93, 9);'
  1455. );
  1456. }
  1457. }
  1458.  
  1459. // Функция скрытия эмодзи в чате
  1460. function hideEmotesForChannel(chatContainer) {
  1461. console.log("[7BTTVFZ Control Emotes Panel] Запуск hideEmotesForChannel");
  1462. const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote');
  1463.  
  1464. emotes.forEach(emote => {
  1465. const emoteUrl = emote.src || '';
  1466. const emoteAlt = emote.getAttribute('alt') || '';
  1467. let blockedEntry = null;
  1468.  
  1469. // Проверяем, заблокирован ли эмодзи
  1470. if (emoteUrl.includes('7tv.app')) {
  1471. blockedEntry = blockedEmotes.find(e => e.platform === '7tv' && e.emoteUrl === emoteUrl);
  1472. } else if (emoteUrl.includes('betterttv.net')) {
  1473. blockedEntry = blockedEmotes.find(e => e.platform === 'bttTV' && e.emoteUrl === emoteUrl);
  1474. } else if (emoteAlt) {
  1475. blockedEntry = blockedChannels.find(e => e.platform === 'TwitchChannel' && emoteAlt.startsWith(e.name));
  1476. }
  1477.  
  1478. // Устанавливаем data-emote-id, если эмодзи заблокирован
  1479. if (blockedEntry && !emote.getAttribute('data-emote-id')) {
  1480. emote.setAttribute('data-emote-id', blockedEntry.id);
  1481. }
  1482.  
  1483. const emoteId = emote.getAttribute('data-emote-id');
  1484. const isBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId));
  1485.  
  1486. // Скрываем или показываем эмодзи
  1487. emote.style.display = isBlocked ? 'none' : '';
  1488. console.log(`[7BTTVFZ Control Emotes Panel] Эмодзи ${emoteAlt || emoteUrl} (ID: ${emoteId || 'не установлен'}) ${isBlocked ? 'скрыт' : 'показан'}`);
  1489. });
  1490. }
  1491.  
  1492. // Функция получения имени эмотикона по умолчанию
  1493. function getDefaultEmoteName(channel) {
  1494. if (channel.platform === '7tv' || channel.platform === 'bttTV') {
  1495. return channel.name.split('/').slice(-2, -1)[0] || 'No Name';
  1496. } else if (channel.platform === 'ffz') {
  1497. return channel.emoteName || channel.name.split('/').pop() || 'No Name';
  1498. } else if (channel.platform === 'TwitchChannel') {
  1499. return channel.name.split(/[^a-zA-Z0-9]/)[0] || 'No Name';
  1500. } else {
  1501. return 'No Name';
  1502. }
  1503. }
  1504.  
  1505.  
  1506.  
  1507.  
  1508.  
  1509. // Добавляем кнопку "Unblock All Emotes" в контейнер кнопок
  1510. const unblockAllButton = document.createElement('button');
  1511. unblockAllButton.innerText = 'Unblock All Emotes';
  1512. unblockAllButton.style.background = buttonColor;
  1513. unblockAllButton.style.color = '#fff';
  1514. unblockAllButton.style.border = 'none';
  1515. unblockAllButton.style.borderRadius = '4px';
  1516. unblockAllButton.style.padding = '5px 10px';
  1517. unblockAllButton.style.cursor = 'pointer';
  1518. unblockAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Unblock All Emotes"
  1519. buttonContainer.appendChild(unblockAllButton);
  1520.  
  1521. // Добавляем кнопку "Back To Block All Emotes" в контейнер кнопок
  1522. const blockAllButton = document.createElement('button');
  1523. blockAllButton.innerText = 'Back To Block All Emotes';
  1524. blockAllButton.style.background = buttonColor;
  1525. blockAllButton.style.color = '#fff';
  1526. blockAllButton.style.border = 'none';
  1527. blockAllButton.style.borderRadius = '4px';
  1528. blockAllButton.style.padding = '5px 10px';
  1529. blockAllButton.style.cursor = 'pointer';
  1530. blockAllButton.style.boxShadow = buttonShadow; // Тень для кнопки "Back To Block All Emotes"
  1531. buttonContainer.appendChild(blockAllButton);
  1532.  
  1533. // Обработчик события для кнопки "Unblock All Emotes"
  1534. unblockAllButton.onclick = () => {
  1535. const unblockedEmotes = GM_getValue('unblockedEmotes', []);
  1536. const unblockedChannels = GM_getValue('unblockedChannels', []);
  1537. if (blockedEmotes.length > 0 || blockedChannels.length > 0) {
  1538. GM_setValue('unblockedEmotes', blockedEmotes);
  1539. GM_setValue('unblockedChannels', blockedChannels);
  1540. blockedEmotes = [];
  1541. blockedChannels = [];
  1542. GM_setValue('blockedEmotes', JSON.stringify(blockedEmotes, null, 2)); // Исправлено
  1543. GM_setValue('blockedChannels', JSON.stringify(blockedChannels, null, 2)); // Исправлено
  1544. console.log("[7BTTVFZ Control Emotes Panel] Разблокированы все: unblockedEmotes:", blockedEmotes, "unblockedChannels:", blockedChannels);
  1545. updateBlockedList();
  1546. updateCounter();
  1547. showAllEmotes();
  1548. }
  1549. };
  1550.  
  1551. // Функция для отображения всех смайлов в чате
  1552. function showAllEmotes() {
  1553. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1554. if (chatContainer) {
  1555. const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote');
  1556. emotes.forEach(emote => {
  1557. emote.style.display = ''; // Сбросить стиль display для отображения смайлов
  1558. });
  1559. }
  1560. }
  1561.  
  1562. // Обработчик события для кнопки "Back To Block All Emotes"
  1563. blockAllButton.onclick = () => {
  1564. const unblockedEmotes = GM_getValue('unblockedEmotes', []);
  1565. const unblockedChannels = GM_getValue('unblockedChannels', []);
  1566. if (unblockedEmotes.length > 0 || unblockedChannels.length > 0) {
  1567. blockedEmotes = unblockedEmotes;
  1568. blockedChannels = unblockedChannels;
  1569. GM_setValue('blockedEmotes', JSON.stringify(blockedEmotes));
  1570. GM_setValue('blockedChannels', JSON.stringify(blockedChannels));
  1571. GM_setValue('unblockedEmotes', []);
  1572. GM_setValue('unblockedChannels', []);
  1573. console.log("[7BTTVFZ Control Emotes Panel] Заблокированы все обратно: blockedEmotes:", blockedEmotes, "blockedChannels:", blockedChannels);
  1574.  
  1575. // Обновляем список и счетчик
  1576. updateBlockedList();
  1577. updateCounter();
  1578.  
  1579. // Применяем скрытие эмодзи в чате
  1580. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1581. if (chatContainer) {
  1582. toggleEmotesInNode(chatContainer);
  1583. console.log("[7BTTVFZ Control Emotes Panel] Применено скрытие эмодзи после восстановления блокировки");
  1584. } else {
  1585. console.log(
  1586. "%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден при восстановлении блокировки",
  1587. 'color:rgb(218, 93, 9); font-weight: bold;',
  1588. 'color: rgb(218, 93, 9);'
  1589. );
  1590. }
  1591. }
  1592. };
  1593.  
  1594.  
  1595.  
  1596. // Кнопка "Show Stats Chart"
  1597. const showStatsButton = document.createElement('button');
  1598. showStatsButton.innerText = 'Show Stats Chart';
  1599. showStatsButton.style.cursor = 'pointer';
  1600. showStatsButton.style.position = 'relative';
  1601. showStatsButton.style.right = '1%';
  1602.  
  1603.  
  1604.  
  1605. // Создаём модальное окно для диаграммы
  1606. const chartModal = document.createElement('div');
  1607. chartModal.style.position = 'fixed';
  1608. chartModal.style.top = '35%';
  1609. chartModal.style.left = '35%';
  1610. chartModal.style.width = '35%';
  1611. chartModal.style.height = '55%';
  1612. chartModal.style.display = 'none'; // Скрыто по умолчанию
  1613. chartModal.style.justifyContent = 'center';
  1614. chartModal.style.alignItems = 'center';
  1615. chartModal.style.zIndex = '10001';
  1616.  
  1617. // Создаём контейнер для диаграммы
  1618. const chartContainer = document.createElement('div');
  1619. 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';
  1620. chartContainer.style.borderRadius = '20px';
  1621. chartContainer.style.boxShadow = '16px 20px 20px 5px #0000008c';
  1622. chartContainer.style.border = '2px solid #24888e';
  1623. chartContainer.style.position = 'relative';
  1624. chartContainer.style.width = '600px';
  1625. chartContainer.style.maxHeight = '80vh';
  1626. chartContainer.style.overflowY = 'auto';
  1627.  
  1628. // Создаём кнопку закрытия модального окна
  1629. const closeChartButton = document.createElement('button');
  1630. closeChartButton.innerText = 'Close';
  1631. closeChartButton.style.position = 'absolute';
  1632. closeChartButton.style.top = '10px';
  1633. closeChartButton.style.right = '10px';
  1634. closeChartButton.style.background = '#944646';
  1635. closeChartButton.style.color = '#fff';
  1636. closeChartButton.style.border = 'none';
  1637. closeChartButton.style.borderRadius = '4px';
  1638. closeChartButton.style.padding = '5px 10px';
  1639. closeChartButton.style.cursor = 'pointer';
  1640. closeChartButton.onclick = () => {
  1641. chartModal.style.display = 'none';
  1642. // Уничтожаем диаграмму, чтобы избежать утечек памяти
  1643. const existingChart = Chart.getChart('statsChart');
  1644. if (existingChart) {
  1645. existingChart.destroy();
  1646. }
  1647. };
  1648.  
  1649. // Создаём элемент canvas для диаграммы
  1650. const chartCanvas = document.createElement('canvas');
  1651. chartCanvas.id = 'statsChart';
  1652. chartCanvas.style.maxWidth = '100%';
  1653. chartCanvas.style.height = '400px';
  1654.  
  1655. // Добавляем элементы в модальное окно
  1656. chartContainer.appendChild(closeChartButton);
  1657. chartContainer.appendChild(chartCanvas);
  1658. chartModal.appendChild(chartContainer);
  1659. document.body.appendChild(chartModal);
  1660.  
  1661. // Обработчик клика на кнопку "Show Stats Chart"
  1662. showStatsButton.onclick = () => {
  1663. // Показываем модальное окно
  1664. chartModal.style.display = 'flex';
  1665.  
  1666. // Собираем данные для диаграммы
  1667. const twitchCount = blockedChannels.length;
  1668. const bttvCount = blockedEmotes.filter(channel => channel.platform === 'bttTV').length;
  1669. const tv7Count = blockedEmotes.filter(channel => channel.platform === '7tv').length;
  1670. const ffzCount = blockedEmotes.filter(channel => channel.platform === 'ffz').length;
  1671.  
  1672. // Данные для диаграммы
  1673. const chartData = {
  1674. labels: ['Twitch', 'BTTV', '7TV', 'FFZ'],
  1675. datasets: [{
  1676. label: 'Blocked Emotes by Platform',
  1677. data: [twitchCount, bttvCount, tv7Count, ffzCount],
  1678. backgroundColor: [
  1679. 'rgba(96, 37, 136, 0.6)', // Twitch
  1680. 'rgba(214, 95, 91, 0.6)', // BTTV
  1681. 'rgba(34, 196, 196, 0.6)', // 7TV
  1682. 'rgba(121, 117, 117, 0.66)' // FFZ
  1683. ],
  1684. borderColor: [
  1685. 'rgb(130, 255, 99)',
  1686. 'rgb(97, 183, 240)',
  1687. 'rgb(255, 238, 86)',
  1688. 'rgb(74, 221, 221)'
  1689. ],
  1690. borderWidth: 1
  1691. }]
  1692. };
  1693.  
  1694. // Уничтожаем старую диаграмму, если она существует
  1695. const existingChart = Chart.getChart('statsChart');
  1696. if (existingChart) {
  1697. existingChart.destroy();
  1698. }
  1699.  
  1700. // Создаём новую диаграмму
  1701. new Chart(chartCanvas, {
  1702. type: 'bar',
  1703. data: chartData,
  1704. options: {
  1705. responsive: true,
  1706. maintainAspectRatio: false,
  1707. plugins: {
  1708. legend: {
  1709. position: 'top',
  1710. labels: {
  1711. color: '#E5E7EB' // Цвет текста легенды
  1712. }
  1713. },
  1714. title: {
  1715. display: true,
  1716. text: 'Blocked Emotes by Platform',
  1717. color: ' #E5E7EB' // Цвет текста заголовка
  1718. }
  1719. },
  1720. scales: {
  1721. y: {
  1722. beginAtZero: true,
  1723. title: {
  1724. display: true,
  1725. text: 'Number of Blocked Emotes',
  1726. color: ' #E5E7EB' // Цвет текста заголовка оси Y
  1727. },
  1728. ticks: {
  1729. color: ' #E5E7EB' // Цвет текста значений на оси Y
  1730. }
  1731. },
  1732. x: {
  1733. title: {
  1734. display: true,
  1735. text: 'Platform',
  1736. color: ' #E5E7EB' // Цвет текста заголовка оси X
  1737. },
  1738. ticks: {
  1739. color: ' #E5E7EB' // Цвет текста значений на оси X
  1740. }
  1741. }
  1742. }
  1743. }
  1744. });
  1745. };
  1746.  
  1747.  
  1748. //======================= Счётчик ========================//
  1749. const counter = document.createElement('div');
  1750. counter.style.display = 'flex';
  1751. counter.style.flexDirection = 'row';
  1752. counter.style.justifyContent = 'center';
  1753. counter.style.width = '460px';
  1754. counter.style.backgroundColor = ' #b69dcf'; // Белый фон
  1755. counter.style.color = ' #4c2a5e'; // Цвет текста (темно-фиолетовый)
  1756. counter.style.border = '3px solid #4c2a5e'; // Граница того же цвета, что и текст
  1757. counter.style.borderRadius = '8px'; // Радиус скругления границы
  1758. counter.style.padding = '5px 0px'; // Отступы для удобства
  1759. counter.style.marginLeft = '6px'; // Отступ слева для отделения от других элементов
  1760. counter.style.fontWeight = 'bold'; // Жирное начертание текста
  1761. counter.style.fontSize = '16px'; // Устанавливаем размер шрифта для лучшей видимости
  1762. counter.style.bottom = '545px'; // Обновленное положение сверху
  1763. counter.style.left = '202px'; // Обновленное положение справа
  1764. counter.style.position = 'relative '; // Относительное позиционирование для точного расположения
  1765.  
  1766. controlPanel.appendChild(counter);
  1767.  
  1768. // Функция для обновления счётчика
  1769. function updateCounter() {
  1770. const twitchCount = blockedChannels.length;
  1771. const bttvCount = blockedEmotes.filter(channel => channel.platform === 'bttTV').length;
  1772. const tv7Count = blockedEmotes.filter(channel => channel.platform === '7tv').length;
  1773. const ffzCount = blockedEmotes.filter(channel => channel.platform === 'ffz').length;
  1774. const totalCount = twitchCount + bttvCount + tv7Count + ffzCount;
  1775. counter.innerText = `Twitch: ${twitchCount} | BTTV: ${bttvCount} | 7TV: ${tv7Count} | FFZ: ${ffzCount} | Total: ${totalCount}`;
  1776. }
  1777.  
  1778. // Добавляем элементы на страницу
  1779. inputContainer.appendChild(input);
  1780. inputContainer.appendChild(addButton);
  1781. controlPanel.appendChild(inputContainer);
  1782.  
  1783. // Перемещаем контейнер кнопок вниз
  1784. controlPanel.appendChild(buttonContainer);
  1785.  
  1786. document.body.appendChild(controlPanel);
  1787.  
  1788. // Вызываем функцию обновления счётчика
  1789. updateCounter();
  1790.  
  1791.  
  1792.  
  1793.  
  1794.  
  1795. // Загружаем сохранённое состояние переключателя из хранилища
  1796.  
  1797. //============= Создаем кнопку "Open Blocker Emote" ===================//
  1798. const openPanelButton = document.createElement('button');
  1799. openPanelButton.innerText = 'panel control emotes';
  1800. openPanelButton.style.fontWeight = 'bold';
  1801. openPanelButton.style.top = '22px';
  1802. openPanelButton.style.right = '1344px';
  1803. openPanelButton.style.position = 'fixed'; // Фиксированное положение
  1804. openPanelButton.style.width = '200px'; // Фиксированная ширина кнопки
  1805. openPanelButton.style.height = '41px'; // Фиксированная высота кнопки
  1806. openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки
  1807. openPanelButton.style.color = ' #171c1c';
  1808. openPanelButton.style.border = 'none'; // Без границ
  1809. openPanelButton.style.borderRadius = '20px'; // Закругленные углы
  1810. openPanelButton.style.padding = '10px';
  1811. openPanelButton.style.cursor = 'pointer';
  1812. openPanelButton.style.zIndex = 10000; // Высокий z-index
  1813. openPanelButton.style.transition = 'background 0.3s ease'; // Плавное изменение фона
  1814. openPanelButton.style.display = 'flex';
  1815. openPanelButton.style.alignItems = 'center';
  1816. openPanelButton.style.justifyContent = 'space-between'; // Чтобы текст и переключатель были по разным краям
  1817.  
  1818. // Создаем контейнер для переключателя (темная рамка)
  1819. const switchContainer = document.createElement('div');
  1820. switchContainer.style.width = '44px'; // Увеличиваем ширину контейнера на 6px
  1821. switchContainer.style.height = '27px'; // Увеличиваем высоту контейнера на 6px
  1822. switchContainer.style.borderRadius = '13px'; // Скругленные углы
  1823. switchContainer.style.backgroundColor = ' #171c1c'; // Темно сеая рамка для кружка
  1824. switchContainer.style.position = 'relative'; // Для абсолютного позиционирования кружка
  1825. switchContainer.style.transition = 'background 0.3s ease'; // Плавное изменение фона контейнера
  1826. openPanelButton.appendChild(switchContainer);
  1827.  
  1828. // Создаем фиолетовый кружок (переключатель кружок )
  1829. const switchCircle = document.createElement('div');
  1830. switchCircle.style.width = '19px'; // Увеличиваем ширину кружка на 3px
  1831. switchCircle.style.height = '19px'; // Увеличиваем высоту кружка на 3px
  1832. switchCircle.style.borderRadius = '50%'; // Кружок
  1833. switchCircle.style.backgroundColor = ' #5d5d5d'; // темно Серый цвет кружка
  1834. switchCircle.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.8)'; // Тень для кружка
  1835. switchCircle.style.position = 'absolute'; // Абсолютное позиционирование внутри контейнера
  1836. switchCircle.style.top = '3px'; // Отступ сверху
  1837. switchCircle.style.left = '3px'; // Отступ слева
  1838. switchCircle.style.transition = 'transform 0.3s ease'; // Плавное движение
  1839. switchContainer.appendChild(switchCircle);
  1840.  
  1841. // Функция для обновления состояния переключателя
  1842. const updateSwitchState = () => {
  1843. if (isPanelOpen) {
  1844. openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки при открытой панели
  1845. switchCircle.style.transform = 'translateX(20px)'; // Перемещаем кружок вправо
  1846. switchContainer.style.backgroundColor = ' #464646'; // Цвет контейнера в включённом состоянии
  1847. controlPanel.style.display = 'block'; // Показываем панель
  1848. controlPanel.style.height = '656px'; // Устанавливаем полную высоту
  1849. } else {
  1850. openPanelButton.style.background = ' #5d5d5d'; // Цвет кнопки при закрытой панели
  1851. switchCircle.style.transform = 'translateX(0)'; // Перемещаем кружок влево
  1852. switchContainer.style.backgroundColor = ' #171c1c'; // Цвет контейнера в выключенном состоянии
  1853. controlPanel.style.display = 'none'; // Скрываем панель
  1854. controlPanel.style.height = '0px'; // Сворачиваем панель
  1855. }
  1856. };
  1857.  
  1858. // Обработчик клика для переключения состояния панели
  1859. openPanelButton.onclick = () => {
  1860. isPanelOpen = !isPanelOpen; // Переключаем состояние
  1861. GM_setValue('isPanelOpen', isPanelOpen); // Сохраняем состояние
  1862. updateSwitchState(); // Обновляем видимость и переключатель
  1863. };
  1864.  
  1865.  
  1866. // Инициализация состояния при загрузке
  1867. window.addEventListener('load', () => {
  1868. document.body.appendChild(openPanelButton);
  1869. updateSwitchState(); // Устанавливаем начальное состояние панели и переключателя
  1870.  
  1871. const updateButtonPosition = () => {
  1872. const windowWidth = window.innerWidth;
  1873. const windowHeight = window.innerHeight;
  1874. openPanelButton.style.top = `${windowHeight * 0.005}px`; // 5% от высоты окна
  1875. openPanelButton.style.right = `${windowWidth * 0.2}px`; // 20% от ширины окна
  1876. };
  1877.  
  1878. updateButtonPosition();
  1879. window.addEventListener('resize', updateButtonPosition);
  1880. });
  1881.  
  1882.  
  1883.  
  1884.  
  1885.  
  1886.  
  1887.  
  1888. //=============== Блокировка и Запуск скрытия эмодзи в чате ==================//
  1889.  
  1890. //=============== Генерация случайного ID ===============//
  1891. function generateRandomID() {
  1892. const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  1893. const randomLength = Math.floor(Math.random() * 67) + 1; // Случайная длина от 1 до 68
  1894. let randomID = '';
  1895. for (let i = 0; i < randomLength; i++) {
  1896. randomID += characters.charAt(Math.floor(Math.random() * characters.length));
  1897. }
  1898. return `emote_${randomID}`;
  1899. }
  1900.  
  1901.  
  1902.  
  1903. // Оптимизированная версия toggleEmotesInNode
  1904. const debouncedToggleEmotes = debounce(toggleEmotesInNode, 100);
  1905.  
  1906. async function toggleEmotesInNode(node) {
  1907. try {
  1908. console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %ctoggleEmotesInNode - starting`,
  1909. 'color: rgb(63, 136, 219);',
  1910. 'color: rgb(52, 163, 148); font-weight: bold;');
  1911.  
  1912. const emotes = node.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote');
  1913. console.log(`[7BTTVFZ Control Emotes Panel] Найдено эмодзи для обработки: ${emotes.length}`);
  1914.  
  1915. for (const emote of emotes) {
  1916. const emoteUrl = emote.src || emote.getAttribute('srcset')?.split(' ')[0] || '';
  1917. const emoteAlt = emote.getAttribute('alt') || '';
  1918. let blockedEntry = null;
  1919.  
  1920. if (emoteUrl.includes('7tv.app')) {
  1921. blockedEntry = blockedEmotes.find(e => e.platform === '7tv' && e.emoteUrl === emoteUrl);
  1922. } else if (emoteUrl.includes('betterttv.net')) {
  1923. blockedEntry = blockedEmotes.find(e => e.platform === 'bttTV' && e.emoteUrl === emoteUrl);
  1924. } else if (emoteUrl.includes('frankerfacez.com')) {
  1925. blockedEntry = blockedEmotes.find(e => e.platform === 'ffz' && e.emoteUrl === emoteUrl);
  1926. } else if (emoteAlt) {
  1927. blockedEntry = blockedChannels.find(e => e.platform === 'TwitchChannel' && emoteAlt.startsWith(e.name));
  1928. }
  1929.  
  1930. if (blockedEntry && !emote.getAttribute('data-emote-id')) {
  1931. emote.setAttribute('data-emote-id', blockedEntry.id);
  1932. }
  1933.  
  1934. const emoteId = emote.getAttribute('data-emote-id');
  1935. const isBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId));
  1936.  
  1937. emote.style.display = isBlocked ? 'none' : '';
  1938. console.log(`[7BTTVFZ Control Emotes Panel] Эмодзи ${emoteAlt || emoteUrl} (ID: ${emoteId || 'не установлен'}) ${isBlocked ? 'скрыт' : 'показан'}`);
  1939. }
  1940.  
  1941. console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %ctoggleEmotesInNode - completed`,
  1942. 'color: rgb(63, 136, 219);',
  1943. 'color: rgb(52, 163, 148); font-weight: bold;');
  1944. } catch (error) {
  1945. console.error(`[ERROR] Ошибка в toggleEmotesInNode:`, error);
  1946. }
  1947. }
  1948. // Используем дебаунс в наблюдателе
  1949. const observer = new MutationObserver(mutations => {
  1950. mutations.forEach(mutation => {
  1951. mutation.addedNodes.forEach(node => {
  1952. if (node.nodeType === 1) {
  1953. console.log(`%cНовый узел добавлен в DOM`,
  1954. 'color:rgb(29, 202, 136) ;');
  1955. debouncedToggleEmotes(node);
  1956. }
  1957. });
  1958. });
  1959. });
  1960.  
  1961.  
  1962.  
  1963. function observeChatContainer() {
  1964. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1965. if (chatContainer) {
  1966. // Успешно - зеленый цвет
  1967. console.log(
  1968. '%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата найден, начинаем наблюдение',
  1969. 'color: #00C4B4; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel]
  1970. 'color: #00C4B4;' // Стиль для остального текста
  1971. );
  1972. observer.disconnect(); // Останавливаем старое наблюдение
  1973. observer.observe(chatContainer, { childList: true, subtree: true });
  1974. toggleEmotesInNode(chatContainer); // Проверяем существующие сообщения
  1975. } else {
  1976. // Неуспешно - красный цвет
  1977. console.log(
  1978. '%c[7BTTVFZ Control Emotes Panel]%c Контейнер чата не найден, повторная попытка через 500мс',
  1979. 'color: #FF5555; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel]
  1980. 'color: #FF5555;' // Стиль для остального текста
  1981. );
  1982. setTimeout(observeChatContainer, 500);
  1983. }
  1984. }
  1985.  
  1986. // Добавляем наблюдение за изменениями на более высоком уровне DOM
  1987. function startRootObserver() {
  1988. const rootObserver = new MutationObserver(() => {
  1989. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  1990. // Состояние контейнера чата - зеленый если найден, красный если не найден
  1991. console.log(
  1992. '%c[7BTTVFZ Control Emotes Panel]%c RootObserver: контейнер чата %c' + (chatContainer ? 'найден' : 'не найден'),
  1993. 'color: #1E90FF; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] (DodgerBlue)
  1994. 'color: #1E90FF;', // Стиль для "RootObserver: контейнер чата"
  1995. `color: ${chatContainer ? '#00C4B4' : '#FF5555'}; font-weight: bold;` // Зеленый (#00C4B4) или красный (#FF5555) для статуса
  1996. );
  1997.  
  1998. if (chatContainer) {
  1999. observeChatContainer();
  2000. }
  2001. });
  2002. rootObserver.observe(document.body, { childList: true, subtree: true });
  2003. // Запуск RootObserver - синий цвет (информационный)
  2004. console.log(
  2005. '%c[7BTTVFZ Control Emotes Panel]%c RootObserver запущен',
  2006. 'color: #1E90FF; font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel] (DodgerBlue)
  2007. 'color: #1E90FF;' // Стиль для остального текста
  2008. );
  2009.  
  2010. }
  2011.  
  2012. // Запускаем наблюдение
  2013. startRootObserver();
  2014.  
  2015.  
  2016. let lastUrl = location.href;
  2017.  
  2018. function checkUrlChange() {
  2019. const currentUrl = location.href;
  2020. if (currentUrl !== lastUrl) {
  2021. console.log('[7BTTVFZ Control Emotes Panel] URL изменился, перезапускаем наблюдение за чатом');
  2022. ContextMenuManager.removeMenu(); // Удаляем контекстное меню
  2023. lastUrl = currentUrl;
  2024. observeChatContainer();
  2025. }
  2026. setTimeout(checkUrlChange, 1000);
  2027. }
  2028.  
  2029. checkUrlChange();
  2030.  
  2031.  
  2032.  
  2033.  
  2034. //=============== Контекстное меню ===============//
  2035. const contextMenuStyle = document.createElement('style');
  2036. contextMenuStyle.innerHTML = `
  2037. .custom-context-menu {
  2038. position: absolute;
  2039. background:rgb(19, 88, 39);
  2040. border: 1px solid #ccc;
  2041. padding: 5px;
  2042. z-index: 10002;
  2043. cursor: pointer;
  2044. color: #fff;
  2045. transition: background 0.3s ease;
  2046. user-select: none;
  2047. min-width: 150px;
  2048. box-shadow: 0 2px 8px 2px #8BC34A;
  2049. border-radius: 8px;
  2050. }
  2051. .custom-context-menu:hover {
  2052. background:rgb(16, 68, 30);
  2053. }
  2054. `;
  2055. document.head.appendChild(contextMenuStyle);
  2056.  
  2057. const ContextMenuManager = {
  2058. menu: null,
  2059. isProcessing: false, // Флаг для предотвращения многократных нажатий
  2060.  
  2061. createMenu(event, emotePrefix, platform, emoteName) {
  2062. this.removeMenu();
  2063. const menu = document.createElement('div');
  2064. menu.className = 'custom-context-menu';
  2065. menu.style.top = `${event.pageY}px`;
  2066. menu.style.left = `${event.pageX}px`;
  2067. menu.innerText = `Block Emote (${emoteName || 'Unknown'})`;
  2068. console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cContext menu created at:`,
  2069. 'color: rgb(85, 113, 165);',
  2070. 'color: rgb(85, 113, 165); font-weight: bold;',
  2071. 'color: rgb(85, 113, 165);', event.pageX, event.pageY);
  2072.  
  2073. document.body.appendChild(menu);
  2074. this.menu = menu;
  2075.  
  2076. menu.addEventListener('click', (e) => {
  2077. e.stopPropagation();
  2078. if (this.isProcessing) return; // Пропускаем, если обработка уже идет
  2079. this.isProcessing = true;
  2080.  
  2081. console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cBlocking emote: ${emoteName}`,
  2082. 'color: rgb(209, 89, 129);',
  2083. 'color: rgb(255, 50, 50); font-weight: bold;',
  2084. 'color: rgb(209, 89, 129);');
  2085.  
  2086. this.blockEmote(emotePrefix, platform, emoteName);
  2087. this.removeMenu();
  2088. this.isProcessing = false;
  2089. });
  2090.  
  2091. document.addEventListener('click', (e) => {
  2092. if (!menu.contains(e.target)) this.removeMenu();
  2093. }, { once: true });
  2094. },
  2095.  
  2096. removeMenu() {
  2097. if (this.menu) {
  2098. console.log(`%c[${new Date().toISOString()}] %c[7BTTVFZ Control Emotes Panel] %cRemoving context menu`,
  2099. 'color: rgb(209, 89, 129);',
  2100. 'color: rgb(115, 2, 160); font-weight: bold;',
  2101. 'color: white;');
  2102. this.menu.remove();
  2103. this.menu = null;
  2104. }
  2105. },
  2106.  
  2107. blockEmote(emotePrefix, platform, emoteName) {
  2108. const emoteId = generateRandomID();
  2109. const currentDateTime = new Date().toISOString();
  2110. const newEntry = {
  2111. id: emoteId,
  2112. name: emotePrefix, // Префикс (например, "guwu")
  2113. platform: platform,
  2114. emoteName: emoteName || emotePrefix.split('/').pop() || 'Unknown', // Полное название (например, "guwuPopcorn")
  2115. emoteUrl: platform === 'TwitchChannel' ? emotePrefix : emotePrefix, // Для Twitch используем префикс как URL
  2116. date: currentDateTime
  2117. };
  2118.  
  2119. const isDuplicate = platform === 'TwitchChannel'
  2120. ? blockedChannels.some(e => e.name === newEntry.name && e.platform === newEntry.platform)
  2121. : blockedEmotes.some(e => e.emoteUrl === newEntry.emoteUrl && e.platform === newEntry.platform);
  2122.  
  2123. if (isDuplicate) {
  2124. console.log(`%c[7BTTVFZ Control Emotes Panel] %cEmote already blocked: ${newEntry.emoteName}`,
  2125. 'color: rgb(255, 165, 0); font-weight: bold;',
  2126. 'color: rgb(255, 165, 0);');
  2127. return;
  2128. }
  2129.  
  2130. if (platform === 'TwitchChannel') {
  2131. blockedChannels.push(newEntry);
  2132. blockedChannelIDs.add(emoteId);
  2133. newlyAddedIds.add(emoteId);
  2134. GM_setValue("blockedChannels", JSON.stringify(blockedChannels, null, 2));
  2135. console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedChannels:`,
  2136. 'color: rgb(0, 255, 0); font-weight: bold;',
  2137. 'color: rgb(0, 255, 0);', newEntry);
  2138. } else {
  2139. blockedEmotes.push(newEntry);
  2140. blockedEmoteIDs.add(emoteId);
  2141. newlyAddedIds.add(emoteId);
  2142. GM_setValue("blockedEmotes", JSON.stringify(blockedEmotes, null, 2));
  2143. console.log(`%c[7BTTVFZ Control Emotes Panel] %cAdded to blockedEmotes:`,
  2144. 'color: rgb(0, 255, 0); font-weight: bold;',
  2145. 'color: rgb(0, 255, 0);', newEntry);
  2146. }
  2147.  
  2148. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  2149. if (chatContainer) {
  2150. toggleEmotesInNode(chatContainer);
  2151. }
  2152.  
  2153. updateBlockedList();
  2154. updateCounter();
  2155. }
  2156. };
  2157.  
  2158. //=============== Обработчик контекстного меню ===============//
  2159. // блокировка сайлов правой кнопкой //
  2160. document.addEventListener('contextmenu', (event) => {
  2161. const target = event.target;
  2162. if (target.tagName === 'IMG' && target.closest('.chat-line__message')) {
  2163. event.preventDefault();
  2164. const emoteUrl = target.src || target.getAttribute('srcset')?.split(' ')[0] || '';
  2165. const emoteAlt = target.getAttribute('alt') || '';
  2166. const dataProvider = target.getAttribute('data-provider') || '';
  2167. let emotePrefix = '';
  2168. let platform = '';
  2169. let emoteName = emoteAlt;
  2170.  
  2171. console.log(`[${new Date().toISOString()}] [7BTTVFZ Control Emotes Panel] Context menu triggered for:`, emoteUrl, emoteAlt, 'data-provider:', dataProvider);
  2172.  
  2173. // Определяем платформу и префикс
  2174. if (dataProvider === 'bttv' && emoteUrl.includes('betterttv.net')) {
  2175. emotePrefix = emoteUrl || `https://cdn.betterttv.net/emote/${target.getAttribute('data-id')}/2x.webp`;
  2176. platform = 'bttTV';
  2177. console.log("[7BTTVFZ Control Emotes Panel] Detected bttv emote (via data-provider):", emotePrefix);
  2178. } else if (dataProvider === 'ffz' && emoteUrl.includes('frankerfacez.com')) {
  2179. emotePrefix = emoteUrl || `https://cdn.frankerfacez.com/emote/${target.getAttribute('data-id')}/2`;
  2180. platform = 'ffz';
  2181. emoteName = emoteAlt;
  2182. console.log("[7BTTVFZ Control Emotes Panel] Detected ffz emote (via data-provider):", emotePrefix);
  2183. } else if (dataProvider === 'ffz' && emoteUrl.includes('7tv.app')) {
  2184. emotePrefix = emoteUrl || `https://cdn.7tv.app/emote/${target.getAttribute('data-id')}/2x.webp`;
  2185. platform = '7tv';
  2186. console.log("[7BTTVFZ Control Emotes Panel] Detected 7tv emote (via data-provider):", emotePrefix);
  2187. } else if (emoteUrl.includes('betterttv.net')) {
  2188. emotePrefix = emoteUrl;
  2189. platform = 'bttTV';
  2190. console.log("[7BTTVFZ Control Emotes Panel] Detected bttv emote (via URL):", emoteUrl);
  2191. } else if (emoteUrl.includes('7tv.app')) {
  2192. emotePrefix = emoteUrl;
  2193. platform = '7tv';
  2194. console.log("[7BTTVFZ Control Emotes Panel] Detected 7tv emote (via URL):", emoteUrl);
  2195. } else if (emoteUrl.includes('frankerfacez.com')) {
  2196. emotePrefix = emoteUrl;
  2197. platform = 'ffz';
  2198. emoteName = emoteAlt;
  2199. console.log("[7BTTVFZ Control Emotes Panel] Detected ffz emote (via URL):", emoteUrl);
  2200. } else if (emoteAlt) {
  2201. // Обновленная логика для TwitchChannel
  2202. const match = emoteAlt.match(/^([a-z0-9]+)([A-Z].*)$/); // Ищем префикс до первой заглавной буквы
  2203. if (match) {
  2204. emotePrefix = match[1]; // Например, "lowti3" из "lowti3Face3"
  2205. emoteName = emoteAlt; // Полное название, например "lowti3Face3"
  2206. } else {
  2207. // Если не удалось разделить, используем первую группу символов до не-букв/цифр как запасной вариант
  2208. emotePrefix = emoteAlt.split(/[^a-zA-Z0-9]/)[0] || emoteAlt;
  2209. emoteName = emoteAlt;
  2210. }
  2211. platform = 'TwitchChannel';
  2212. console.log("[7BTTVFZ Control Emotes Panel] Detected TwitchChannel emote:", emoteAlt, "prefix:", emotePrefix);
  2213. }
  2214.  
  2215. if (emotePrefix && platform) {
  2216. console.log(`[7BTTVFZ Control Emotes Panel] Creating context menu for emote with prefix: ${emotePrefix}, platform: ${platform}`);
  2217. ContextMenuManager.createMenu(event, emotePrefix, platform, emoteName);
  2218. } else {
  2219. console.log("[7BTTVFZ Control Emotes Panel] Could not determine platform or prefix, using fallback TwitchChannel");
  2220. ContextMenuManager.createMenu(event, emoteAlt || emoteUrl, 'TwitchChannel', emoteAlt || 'Unknown');
  2221. }
  2222. }
  2223. });
  2224.  
  2225. //=============== Запуск ===============//
  2226. observeChatContainer();
  2227.  
  2228.  
  2229.  
  2230.  
  2231.  
  2232.  
  2233.  
  2234. //====================== Управление высотой панели =======================
  2235. function closePanel() {
  2236. isPanelOpen = false;
  2237. GM_setValue('isPanelOpen', isPanelOpen);
  2238. controlPanel.style.height = '0px'; // Плавно уменьшаем высоту
  2239. setTimeout(() => {
  2240. if (!isPanelOpen) controlPanel.style.display = 'none'; // Полностью скрываем после завершения анимации
  2241. }, 150); // Таймер соответствует времени анимации
  2242. }
  2243.  
  2244. //----------------- Анимация сворачивания панели-------------------------
  2245. function openPanel() {
  2246. isPanelOpen = true;
  2247. GM_setValue('isPanelOpen', isPanelOpen);
  2248. controlPanel.style.display = 'block'; // Делаем панель видимой
  2249. setTimeout(() => {
  2250. controlPanel.style.height = '656px'; // Плавно увеличиваем высоту
  2251. }, 0); // Устанавливаем высоту с задержкой для работы анимации
  2252. }
  2253.  
  2254. //========================== Переключение состояния панели Управления 'openPanelButton' ===============================
  2255. openPanelButton.onclick = () => {
  2256. isPanelOpen = !isPanelOpen; // Переключаем состояние панели (открыта/закрыта)
  2257. GM_setValue('isPanelOpen', isPanelOpen);
  2258.  
  2259. // Перемещаем переключатель (круглый элемент), когда панель открывается или закрывается
  2260. switchCircle.style.transform = isPanelOpen ? 'translateX(20px)' : 'translateX(0)';
  2261.  
  2262. // Меняем цвет фона контейнера в зависимости от состояния панели
  2263. // switchContainer.style.backgroundColor = isPanelOpen ? ' #9e9e9e' : ' #171c1c'; //
  2264. // закоментируем убрав временно для будущих версий switchContainer //
  2265.  
  2266. // Переключаем видимость панели: открываем или закрываем
  2267. if (isPanelOpen) {
  2268. openPanel(); // Вызов функции для открытия панели
  2269. } else {
  2270. closePanel(); // Вызов функции для закрытия панели
  2271. }
  2272. };
  2273.  
  2274. // Инициализация состояния
  2275. updateSwitchState(); // Убедимся, что переключатель синхронизирован с начальным состоянием
  2276. updateBlockedList();
  2277. updateCounter();
  2278.  
  2279.  
  2280.  
  2281.  
  2282.  
  2283. //============== Минипанель с кнопками сортировки по категориям =================//
  2284. const sortContainer = document.createElement('div');
  2285. sortContainer.id = 'sortContainer';
  2286. sortContainer.style.display = 'flex';
  2287. sortContainer.style.justifyContent = 'space-around';
  2288. sortContainer.style.backgroundColor = ' rgb(89 51 114)';
  2289. sortContainer.style.padding = '5px';
  2290. sortContainer.style.marginBottom = '37px';
  2291. sortContainer.style.position = 'relative';
  2292. sortContainer.style.top = '57px';
  2293. sortContainer.style.borderRadius = '8px 8px 0 0'; // Закругление только верхних углов
  2294. sortContainer.style.border = '1px solid rgb(255, 255, 255)';
  2295. sortContainer.style.boxShadow = ' rgb(0 0 0 / 0%) 0px 15px 6px 0px'; // Использование RGBA для прозрачности
  2296. sortContainer.style.zIndex = 'inherit'; // Наследует z-index от родителя
  2297.  
  2298. // Определение начальных значений для currentSortOrder
  2299. let currentSortOrder = {
  2300. name: 'asc',
  2301. platform: 'asc',
  2302. date: 'asc'
  2303. };
  2304.  
  2305. // Кнопки сортировки
  2306. const sortByNameButton = document.createElement('button');
  2307. sortByNameButton.innerHTML = 'Name ▲';
  2308. sortByNameButton.style.cursor = 'pointer';
  2309. sortByNameButton.style.position = 'relative';
  2310. sortByNameButton.style.left = '13%';
  2311.  
  2312. sortByNameButton.onclick = () => {
  2313. const order = currentSortOrder.name === 'asc' ? 'desc' : 'asc';
  2314. currentSortOrder.name = order;
  2315. sortByNameButton.innerHTML = `Name ${order === 'asc' ? '▲' : '▼'}`; // Переключение стрелочки
  2316. sortblockedEmotes('name', order);
  2317. };
  2318. sortContainer.appendChild(sortByNameButton);
  2319.  
  2320. const sortByPlatformButton = document.createElement('button');
  2321. sortByPlatformButton.innerHTML = 'Platform ▲';
  2322. sortByPlatformButton.style.cursor = 'pointer';
  2323. sortByPlatformButton.style.position = 'relative';
  2324. sortByPlatformButton.style.right = '118px';
  2325.  
  2326. sortByPlatformButton.onclick = () => {
  2327. const order = currentSortOrder.platform === 'asc' ? 'desc' : 'asc';
  2328. currentSortOrder.platform = order;
  2329. sortByPlatformButton.innerHTML = `Platform ${order === 'asc' ? '▲' : '▼'}`;
  2330. sortblockedEmotes('platform', order);
  2331. };
  2332. sortContainer.appendChild(sortByPlatformButton);
  2333.  
  2334. const sortByDateButton = document.createElement('button');
  2335. sortByDateButton.innerHTML = 'Date-Time ▲';
  2336. sortByDateButton.style.cursor = 'pointer';
  2337. sortByDateButton.style.top = '0px';
  2338. sortByDateButton.style.position = 'relative';
  2339. sortByDateButton.style.left = '9px';
  2340. sortByDateButton.onclick = () => {
  2341. const order = currentSortOrder.date === 'asc' ? 'desc' : 'asc';
  2342. currentSortOrder.date = order;
  2343. sortByDateButton.innerHTML = `Date ${order === 'asc' ? '▲' : '▼'}`;
  2344. sortblockedEmotes('date', order);
  2345. };
  2346. sortContainer.appendChild(sortByDateButton);
  2347.  
  2348. // Добавляем контейнер сортировки в панель управления
  2349. controlPanel.insertBefore(sortContainer, title);
  2350.  
  2351.  
  2352. // ---------- goToLast Button ------------- //
  2353. const goToLastButton = document.createElement('button');
  2354. goToLastButton.innerHTML = 'Go To Last Element ▼'; // Короткое название
  2355. goToLastButton.style.cursor = 'pointer';
  2356. goToLastButton.style.position = 'relative';
  2357. goToLastButton.style.right = '2%'; // Сдвигаем чуть левее для баланса
  2358.  
  2359. goToLastButton.onclick = () => {
  2360. goToLastAddedItem();
  2361. };
  2362. sortContainer.appendChild(goToLastButton);
  2363. sortContainer.appendChild(showStatsButton);
  2364.  
  2365.  
  2366.  
  2367.  
  2368. //============== Функция для сортировки списка =================//
  2369. function sortblockedEmotes(criteria, order) {
  2370. const sortFunc = (a, b) => {
  2371. let comparison = 0;
  2372. if (criteria === 'name') {
  2373. comparison = a.emoteName.localeCompare(b.emoteName);
  2374. } else if (criteria === 'platform') {
  2375. comparison = a.platform.localeCompare(b.platform);
  2376. } else if (criteria === 'date') {
  2377. comparison = new Date(a.date) - new Date(b.date);
  2378. }
  2379. return order === 'asc' ? comparison : -comparison;
  2380. };
  2381.  
  2382. // Сортируем оба массива
  2383. blockedEmotes.sort(sortFunc);
  2384. blockedChannels.sort(sortFunc);
  2385.  
  2386. // Обновляем интерфейс после сортировки
  2387. updateBlockedList();
  2388. }
  2389. //============== Обработчики событий для кнопок =================//
  2390. const buttons = [addButton, clearAllButton, exportButton, importButton, unblockAllButton, blockAllButton];
  2391. buttons.forEach(button => {
  2392. button.onmouseover = function() {
  2393. button.style.background = '-webkit-linear-gradient(135deg, #443157 0%,rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)'; // Изменение фона при наведении
  2394. };
  2395. button.onmouseout = function() {
  2396. button.style.background = buttonColor; // Возвращаем исходный цвет
  2397. };
  2398. });
  2399.  
  2400.  
  2401.  
  2402. // ========= Функция для прокрутки к последнему добавленному элементу ============= //
  2403. function goToLastAddedItem() {
  2404. const allItems = [...blockedEmotes, ...blockedChannels];
  2405. if (allItems.length === 0) {
  2406. console.log("[7BTTVFZ Control Emotes Panel] Список пуст, некуда прокручивать");
  2407. return;
  2408. }
  2409.  
  2410. // Находим элемент с самой поздней датой
  2411. const lastItem = allItems.reduce((latest, current) => {
  2412. return new Date(current.date) > new Date(latest.date) ? current : latest;
  2413. });
  2414.  
  2415. // Ищем элемент в DOM по его ID
  2416. let lastElement = list.querySelector(`[data-id="${lastItem.id}"]`);
  2417. if (lastElement) {
  2418. // Подсвечиваем элемент
  2419. lastElement.classList.add('last-item-highlight');
  2420.  
  2421. // Прокручиваем к элементу
  2422. const itemOffsetTop = lastElement.offsetTop;
  2423. const listHeight = list.clientHeight;
  2424. const itemHeight = lastElement.clientHeight;
  2425. const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2);
  2426. list.scrollTo({
  2427. top: scrollPosition,
  2428. behavior: 'smooth'
  2429. });
  2430.  
  2431. // Убираем подсветку через 60 секунд
  2432. setTimeout(() => {
  2433. lastElement.classList.remove('last-item-highlight');
  2434. console.log(`[7BTTVFZ Control Emotes Panel] Подсветка убрана с элемента: ${lastItem.emoteName}`);
  2435. }, 60000); // 60000 мс = 1 минута
  2436.  
  2437. console.log(`[7BTTVFZ Control Emotes Panel] Прокручено и подсвечено: ${lastItem.emoteName} (ID: ${lastItem.id})`);
  2438. } else {
  2439. console.log("[7BTTVFZ Control Emotes Panel] Последний элемент не найден в DOM, обновляем список");
  2440. updateBlockedList();
  2441. setTimeout(() => {
  2442. lastElement = list.querySelector(`[data-id="${lastItem.id}"]`);
  2443. if (lastElement) {
  2444. lastElement.classList.add('last-item-highlight');
  2445. const itemOffsetTop = lastElement.offsetTop;
  2446. const listHeight = list.clientHeight;
  2447. const itemHeight = lastElement.clientHeight;
  2448. const scrollPosition = itemOffsetTop - (listHeight / 2) + (itemHeight / 2);
  2449. list.scrollTo({
  2450. top: scrollPosition,
  2451. behavior: 'smooth'
  2452. });
  2453. setTimeout(() => {
  2454. lastElement.classList.remove('last-item-highlight');
  2455. console.log(`[7BTTVFZ Control Emotes Panel] Подсветка убрана с элемента после обновления: ${lastItem.emoteName}`);
  2456. }, 60000);
  2457. console.log(`[7BTTVFZ Control Emotes Panel] Успешно прокручено и подсвечено после обновления: ${lastItem.emoteName}`);
  2458. }
  2459. }, 100);
  2460. }
  2461. }
  2462.  
  2463. console.log(getComputedStyle(controlPanel).display);
  2464. console.log("[7BTTVFZ Control Emotes Panel] Opening control panel...");
  2465. console.log("[7BTTVFZ Control Emotes Panel] Creating control panel...");
  2466. console.log("[7BTTVFZ Control Emotes Panel] Adding button...");
  2467. console.log("[7BTTVFZ Control Emotes Panel] Updating channel list...");
  2468. console.log("[7BTTVFZ Control Emotes Panel] Creating file input element...");
  2469. // Удаляем некорректные логи с event, так как они не в контексте события
  2470. console.log("[7BTTVFZ Control Emotes Panel] Processing imported channels...");
  2471. console.log("[7BTTVFZ Control Emotes Panel] Updating interface...");
  2472. console.log("[7BTTVFZ Control Emotes Panel] Showing all emotes in chat...");
  2473. console.log("[7BTTVFZ Control Emotes Panel] Blocking all emotes...");
  2474. console.log("[7BTTVFZ Control Emotes Panel] Hiding emotes for a channel...");
  2475. console.log(`%c[7BTTVFZ Control Emotes Panel] %cWaiting for chat container...`,
  2476. 'color:rgb(255, 114, 173); font-weight: bold;', // Стиль для [7BTTVFZ Control Emotes Panel]
  2477. 'color: rgb(255, 114, 173) ;'); // Стиль для остального текста
  2478.  
  2479. console.log("[7BTTVFZ Control Emotes Panel] Creating context menu...");
  2480.  
  2481.  
  2482.  
  2483.  
  2484.  
  2485. // Добавляем переменные для отслеживания состояния
  2486. let lastKnownBlockedCount = blockedEmotes.length + blockedChannels.length;
  2487. let lastCheckTime = Date.now();
  2488. let isRestarting = false;
  2489.  
  2490. // Функция проверки состояния блокировки
  2491. function checkBlockingStatus() {
  2492. console.log(`%c[WATCHDOG] %cПроверка состояния блокировки...`,
  2493. 'color:rgb(221, 101, 175); font-weight: bold;',
  2494. 'color: rgb(164, 207, 44) ;');
  2495.  
  2496. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  2497. if (!chatContainer) {
  2498. console.log(
  2499. "%c[WATCHDOG]%c Контейнер чата не найден, перезапускаем наблюдение",
  2500. 'color:rgb(172, 147, 223); font-weight: bold;',
  2501. 'color: rgb(164, 207, 44) ;');
  2502. observeChatContainer(); // Перезапускаем наблюдение
  2503. return false;
  2504. }
  2505.  
  2506. const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote');
  2507. if (emotes.length === 0) {
  2508. console.log("[WATCHDOG] Эмодзи в чате не найдены, пропускаем проверку");
  2509. return true;
  2510. }
  2511.  
  2512. let failureDetected = false;
  2513.  
  2514. emotes.forEach((emote, index) => {
  2515. if (index > 5) return;
  2516. const emoteId = emote.getAttribute('data-emote-id');
  2517. const shouldBeBlocked = emoteId && (blockedChannels.some(e => e.id === emoteId) || blockedEmotes.some(e => e.id === emoteId));
  2518. const isVisible = emote.style.display !== 'none';
  2519.  
  2520. if (shouldBeBlocked && isVisible) {
  2521. console.log(`[WATCHDOG] Обнаружен сбой: эмодзи с ID ${emoteId} должен быть скрыт, но виден!`);
  2522. failureDetected = true;
  2523. } else if (!shouldBeBlocked && !isVisible) {
  2524. console.log(`[WATCHDOG] Обнаружен сбой: эмодзи с ID ${emoteId} не должен быть скрыт, но скрыт!`);
  2525. failureDetected = true;
  2526. }
  2527. });
  2528.  
  2529. const currentBlockedCount = blockedEmotes.length + blockedChannels.length;
  2530. if (currentBlockedCount !== lastKnownBlockedCount) {
  2531. console.log(
  2532. `%c[WATCHDOG] %cКоличество заблокированных элементов изменилось: %c${lastKnownBlockedCount} -> ${currentBlockedCount}`,
  2533. 'color: rgb(221, 101, 175); font-weight: bold;',
  2534. 'color: rgb(164, 207, 44);',
  2535. 'color: rgb(255, 165, 0); font-weight: bold;'
  2536. );
  2537. lastKnownBlockedCount = currentBlockedCount;
  2538. }
  2539.  
  2540. return !failureDetected;
  2541. }
  2542.  
  2543. function showNotification(message, duration = 3000) {
  2544. const notification = document.createElement('div');
  2545. notification.innerText = message; // Добавляем текст
  2546. notification.style.position = 'relative';
  2547. notification.style.bottom = '99%';
  2548. notification.style.maxWidth = '155px';
  2549. notification.style.left = '61%';
  2550. notification.style.backgroundColor = '#341d41';
  2551. notification.style.color = ' #30aa54';
  2552. notification.style.padding = '6px';
  2553. notification.style.borderRadius = '40px';
  2554. notification.style.boxShadow = 'rgb(130, 113, 148) 1px 1px 7px 4px';
  2555. notification.style.zIndex = '1001';
  2556. notification.style.fontSize = '10px';
  2557.  
  2558. // Начальные стили для анимации (уменьшенный размер)
  2559. notification.style.transform = 'scale(0)'; // Начинаем с масштаба 0
  2560. notification.style.opacity = '0'; // Начинаем с прозрачности 0
  2561. notification.style.transition = 'transform 0.3s ease, opacity 0.3s ease'; // Плавный переход для масштаба и прозрачности
  2562.  
  2563. document.body.appendChild(notification);
  2564.  
  2565. // Запускаем анимацию увеличения после добавления в DOM
  2566. setTimeout(() => {
  2567. notification.style.transform = 'scale(1)'; // Увеличиваем до нормального размера
  2568. notification.style.opacity = '1'; // Делаем полностью видимым
  2569. }, 10); // Небольшая задержка для запуска перехода
  2570.  
  2571. // Удаляем уведомление после завершения длительности
  2572. setTimeout(() => {
  2573. // Добавляем анимацию исчезновения перед удалением (опционально)
  2574. notification.style.transform = 'scale(0)';
  2575. notification.style.opacity = '0';
  2576. setTimeout(() => {
  2577. notification.remove();
  2578. }, 300); // Соответствует времени transition
  2579. }, duration);
  2580. }
  2581.  
  2582. // Функция перезапуска логики блокировки
  2583. function restartBlockingLogic() {
  2584. if (isRestarting) return;
  2585. isRestarting = true;
  2586. // Перезапуск логики - оранжевый цвет (в процессе)
  2587. console.log(
  2588. '%c[WATCHDOG]%c Перезапуск логики блокировки...',
  2589. 'color: #FF4500; font-weight: bold;', // Стиль для [WATCHDOG] (OrangeRed)
  2590. 'color: #FF4500;' // Стиль для остального текста
  2591. );
  2592. showNotification(" chat not found ... waiting... ", 3000); // уведомление о перезапуске когда сбой failure
  2593.  
  2594. observer.disconnect();
  2595. const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
  2596. if (chatContainer) {
  2597. const emotes = chatContainer.querySelectorAll('.chat-line__message img, .chat-line__message .emote, .chat-line__message .bttv-emote, .chat-line__message .seventv-emote');
  2598. emotes.forEach(emote => {
  2599. emote.style.display = '';
  2600. emote.removeAttribute('data-emote-id');
  2601. });
  2602. observeChatContainer();
  2603. toggleEmotesInNode(chatContainer);
  2604. } else {
  2605. observeChatContainer();
  2606. }
  2607.  
  2608. updateBlockedList();
  2609. updateCounter();
  2610. setTimeout(() => {
  2611. isRestarting = false;
  2612. // Перезапуск завершен - зеленый цвет (успех)
  2613. console.log(
  2614. '%c[WATCHDOG]%c Перезапуск завершен',
  2615. 'color: #00C4B4; font-weight: bold;', // Стиль для [WATCHDOG] (Teal)
  2616. 'color: #00C4B4;' // Стиль для остального текста
  2617. );
  2618. }, 1000); // Задержка для предотвращения спама
  2619. }
  2620.  
  2621. // Периодическая проверка состояния (watchdog)
  2622. function startWatchdog() {
  2623. const interval = GM_getValue('watchdogInterval', 10) * 1000; // Секунды в миллисекунды
  2624. setInterval(() => {
  2625. const currentTime = Date.now();
  2626. if (currentTime - lastCheckTime < 5000) return;
  2627. lastCheckTime = currentTime;
  2628.  
  2629. const isWorking = checkBlockingStatus();
  2630. if (!isWorking) {
  2631. console.log(
  2632. '%c[WATCHDOG]%c Обнаружен сбой в работе блокировки, перезапуск...',
  2633. 'color: #FFA500; font-weight: bold;',
  2634. 'color: #FFA500;'
  2635. );
  2636. restartBlockingLogic();
  2637. } else {
  2638. console.log(
  2639. `%c[WATCHDOG] %cБлокировка работает корректно!`,
  2640. 'color:rgb(6, 167, 0); font-weight: bold;',
  2641. 'color: rgb(164, 207, 44);'
  2642. );
  2643. }
  2644. }, interval); // Используем динамический интервал
  2645. }
  2646.  
  2647.  
  2648. //================ Модуль управления темами ================== //
  2649. (function () {
  2650. // Определяем начальный массив тем
  2651. //================ Модуль управления темами ================== //
  2652. (function () {
  2653. // Резервные темы (используются, если загрузка с GitHub не удалась)
  2654. const fallbackThemes = [
  2655. {
  2656. name: 'default',
  2657. displayName: 'Default Theme',
  2658. styles: {
  2659. controlPanel: {
  2660. 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%)',
  2661. border: '1px solid #ccc',
  2662. boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
  2663. color: '#fff'
  2664. },
  2665. lastItemHighlight: {
  2666. backgroundColor: '#ffd700'
  2667. },
  2668. openPanelButton: {
  2669. background: '#4c2a5e',
  2670. color: '#bda3d7',
  2671. border: 'none'
  2672. },
  2673. switchContainer: {
  2674. backgroundColor: '#ccb8eb5c',
  2675. activeBackgroundColor: '#bda3d7'
  2676. },
  2677. switchCircle: {
  2678. backgroundColor: '#4c2a5e',
  2679. boxShadow: '0 2px 6px rgba(0, 0, 0, 0.8)'
  2680. },
  2681. list: {
  2682. background: '-webkit-linear-gradient(45deg, hsla(292, 44%, 16%, 1) 0%, hsla(173, 29%, 48%, 1) 100%)',
  2683. border: '1px solid #ffffff',
  2684. color: '#fff',
  2685. scrollbarThumb: '#C1A5EF',
  2686. scrollbarTrack: '#455565'
  2687. },
  2688. counter: {
  2689. backgroundColor: '#b69dcf',
  2690. color: '#4c2a5e',
  2691. border: '3px solid #4c2a5e'
  2692. },
  2693. sortContainer: {
  2694. backgroundColor: 'rgb(89, 51, 114)',
  2695. border: '1px solid rgb(255, 255, 255)',
  2696. color: '#fff'
  2697. },
  2698. title: {
  2699. color: '#2a1e38'
  2700. },
  2701. buttons: {
  2702. background: '#907cad',
  2703. color: '#fff'
  2704. },
  2705. versionLabel: {
  2706. color: 'rgb(62, 33, 85)'
  2707. },
  2708. searchInput: {
  2709. background: '#192427',
  2710. color: '#b69dcf',
  2711. border: '1px solid #b69dcf'
  2712. },
  2713. input: {
  2714. background: '#192427',
  2715. color: '#b69dcf',
  2716. border: '1px solid #b69dcf'
  2717. },
  2718. themeSelect: {
  2719. background: '#192427',
  2720. color: '#b69dcf',
  2721. border: '1px solid #c1a5ef'
  2722. },
  2723. platformSelect: {
  2724. background: '#192427',
  2725. color: '#b69dcf',
  2726. border: '1px solid #c1a5ef'
  2727. },
  2728. deleteButton: {
  2729. background: '#944646',
  2730. color: '#fff',
  2731. hoverBackground: 'linear-gradient(135deg, #480a0c 56%, #ca5d5f 98%, #8b4040 100%)'
  2732. },
  2733. listItemText: {
  2734. color: '#ffffff'
  2735. },
  2736. listItemLink: {
  2737. color: '#b3e0f2'
  2738. },
  2739. listItemDate: {
  2740. color: '#cccccc'
  2741. },
  2742. chartWrapper: {
  2743. backgroundColor: '#fff',
  2744. border: '1px solid #ffffff',
  2745. color: '#000'
  2746. }
  2747. }
  2748. }
  2749. ];
  2750.  
  2751. // Загружаем кэшированные темы или используем резервные
  2752. let themes = GM_getValue('themes', fallbackThemes);
  2753. let selectedThemeName = GM_getValue('selectedTheme', 'default');
  2754.  
  2755. // Функция для загрузки тем с GitHub
  2756. async function loadThemesFromGitHub() {
  2757. return new Promise((resolve, reject) => {
  2758. GM_xmlhttpRequest({
  2759. method: 'GET',
  2760. url: 'https://raw.githubusercontent.com/sopernik566/Control_Emotes_Panel_Twitch_JS/refs/heads/main/themes.json',
  2761. onload: function (response) {
  2762. try {
  2763. const loadedThemes = JSON.parse(response.responseText);
  2764. console.log('[7BTV Control Emotes Panel] Темы загружены с GitHub:', loadedThemes);
  2765. themes = loadedThemes;
  2766. GM_setValue('themes', themes); // Кэшируем темы локально
  2767. resolve(themes);
  2768. } catch (err) {
  2769. console.error('[7BTV Control Emotes Panel] Ошибка парсинга тем с GitHub:', err);
  2770. reject(err);
  2771. }
  2772. },
  2773. onerror: function (err) {
  2774. console.error('[7BTV Control Emotes Panel] Ошибка загрузки тем с GitHub:', err);
  2775. reject(err);
  2776. }
  2777. });
  2778. });
  2779. }
  2780.  
  2781. // Функция для сохранения тем в хранилище
  2782. function saveThemes() {
  2783. GM_setValue('themes', themes);
  2784. console.log('[7BTV Control Emotes Panel] Темы сохранены в хранилище:', themes);
  2785. }
  2786.  
  2787. // Функция для сохранения выбранной темы
  2788. function saveSelectedTheme(themeName) {
  2789. selectedThemeName = themeName;
  2790. GM_setValue('selectedTheme', themeName);
  2791. console.log('[7BTV Control Emotes Panel] Выбранная тема сохранена:', themeName);
  2792. }
  2793.  
  2794. // Функция для применения темы
  2795. function applyTheme(themeName) {
  2796. const theme = themes.find(t => t.name === themeName) || themes[0];
  2797. if (!theme) {
  2798. console.warn(`[7BTV Control Emotes Panel] Тема ${themeName} не найдена, используется 'default'`);
  2799. applyTheme('default');
  2800. return;
  2801. }
  2802.  
  2803. console.log(`[7BTV Control Emotes Panel] Применение темы: ${themeName}`);
  2804.  
  2805. const currentPanelDisplay = controlPanel.style.display;
  2806. const currentPanelHeight = controlPanel.style.height;
  2807.  
  2808. if (openPanelButton) {
  2809. Object.assign(openPanelButton.style, theme.styles.openPanelButton);
  2810. openPanelButton.style.position = 'fixed';
  2811. openPanelButton.style.zIndex = '10000';
  2812. openPanelButton.style.transition = 'background 0.3s ease';
  2813. }
  2814.  
  2815. if (switchContainer) {
  2816. Object.assign(switchContainer.style, {
  2817. backgroundColor: isPanelOpen ? theme.styles.switchContainer.activeBackgroundColor : theme.styles.switchContainer.backgroundColor,
  2818. width: '44px',
  2819. height: '27px',
  2820. borderRadius: '13px',
  2821. position: 'relative',
  2822. transition: 'background 0.3s ease'
  2823. });
  2824. }
  2825.  
  2826. if (switchCircle) {
  2827. Object.assign(switchCircle.style, theme.styles.switchCircle);
  2828. switchCircle.style.width = '19px';
  2829. switchCircle.style.height = '19px';
  2830. switchCircle.style.borderRadius = '50%';
  2831. switchCircle.style.position = 'absolute';
  2832. switchCircle.style.top = '3px';
  2833. switchCircle.style.left = '3px';
  2834. switchCircle.style.transition = 'transform 0.3s ease';
  2835. }
  2836.  
  2837. if (controlPanel) {
  2838. Object.assign(controlPanel.style, theme.styles.controlPanel);
  2839. controlPanel.style.display = currentPanelDisplay;
  2840. controlPanel.style.height = currentPanelHeight;
  2841. controlPanel.style.transition = 'height 0.3s ease';
  2842. }
  2843.  
  2844. if (list) {
  2845. Object.assign(list.style, theme.styles.list);
  2846. const styleElement = document.getElementById('customScrollbarStyle') || document.createElement('style');
  2847. styleElement.id = 'customScrollbarStyle';
  2848. styleElement.innerHTML = `
  2849. #blockedList::-webkit-scrollbar { width: 25px; }
  2850. #blockedList::-webkit-scrollbar-thumb {
  2851. background-color: ${theme.styles.list.scrollbarThumb};
  2852. border-radius: 8px;
  2853. border: 3px solid #4F3E6A;
  2854. height: 80px;
  2855. }
  2856. #blockedList::-webkit-scrollbar-thumb:hover { background-color: ${theme.styles.list.scrollbarThumb}; }
  2857. #blockedList::-webkit-scrollbar-thumb:active { background-color: ${theme.styles.list.scrollbarThumb}; }
  2858. #blockedList::-webkit-scrollbar-track {
  2859. background: ${theme.styles.list.scrollbarTrack};
  2860. border-radius: 0px 0px 8px 0px;
  2861. }
  2862. #blockedList::-webkit-scrollbar-track:hover { background: ${theme.styles.list.scrollbarTrack}; }
  2863. #blockedList::-webkit-scrollbar-track:active { background: ${theme.styles.list.scrollbarTrack}; }
  2864. `;
  2865. if (!document.getElementById('customScrollbarStyle')) {
  2866. document.head.appendChild(styleElement);
  2867. }
  2868. }
  2869.  
  2870. const lastItemHighlightStyle = document.createElement('style');
  2871. lastItemHighlightStyle.id = 'lastItemHighlightStyle';
  2872. lastItemHighlightStyle.innerHTML = `
  2873. .last-item-highlight {
  2874. background-color: ${theme.styles.lastItemHighlight?.backgroundColor || '#ffd700'};
  2875. transition: background-color 0.5s ease;
  2876. }
  2877. `;
  2878. const existingStyle = document.getElementById('lastItemHighlightStyle');
  2879. if (existingStyle) {
  2880. existingStyle.remove();
  2881. }
  2882. document.head.appendChild(lastItemHighlightStyle);
  2883.  
  2884. if (counter) {
  2885. Object.assign(counter.style, theme.styles.counter);
  2886. counter.style.display = 'flex';
  2887. }
  2888.  
  2889. if (sortContainer) {
  2890. Object.assign(sortContainer.style, theme.styles.sortContainer);
  2891. sortContainer.style.display = 'flex';
  2892. }
  2893.  
  2894. if (title) {
  2895. Object.assign(title.style, theme.styles.title);
  2896. title.style.display = 'block';
  2897. }
  2898.  
  2899. const buttons = [addButton, clearAllButton, exportButton, importButton, unblockAllButton, blockAllButton, searchButton];
  2900. buttons.forEach(button => {
  2901. Object.assign(button.style, theme.styles.buttons);
  2902. button.onmouseover = () => {
  2903. button.style.background = '-webkit-linear-gradient(135deg, #443157 0%, rgb(90, 69, 122) 56%, #443157 98%, #443157 100%)';
  2904. };
  2905. button.onmouseout = () => {
  2906. button.style.background = theme.styles.buttons.background;
  2907. };
  2908. });
  2909.  
  2910. if (versionLabel) {
  2911. Object.assign(versionLabel.style, theme.styles.versionLabel);
  2912. }
  2913.  
  2914. if (searchInput) {
  2915. Object.assign(searchInput.style, theme.styles.searchInput);
  2916. }
  2917.  
  2918. if (input) {
  2919. Object.assign(input.style, theme.styles.input);
  2920. }
  2921.  
  2922. if (platformSelect) {
  2923. Object.assign(platformSelect.style, theme.styles.platformSelect);
  2924. }
  2925.  
  2926. if (themeSelect) {
  2927. Object.assign(themeSelect.style, theme.styles.themeSelect);
  2928. }
  2929.  
  2930. const deleteButtons = list.querySelectorAll('.delete-button');
  2931. deleteButtons.forEach(button => {
  2932. Object.assign(button.style, theme.styles.deleteButton, {
  2933. height: '35px',
  2934. width: '75px',
  2935. fontWeight: 'bold',
  2936. fontSize: '16px',
  2937. border: 'none',
  2938. borderRadius: '4px',
  2939. cursor: 'pointer',
  2940. boxShadow: '0 4px 8px rgba(0, 0, 0, 0.6)',
  2941. display: 'flex',
  2942. alignItems: 'center',
  2943. justifyContent: 'center',
  2944. transition: 'background 0.3s ease'
  2945. });
  2946. button.onmouseover = () => {
  2947. button.style.background = theme.styles.deleteButton.hoverBackground;
  2948. };
  2949. button.onmouseout = () => {
  2950. button.style.background = theme.styles.deleteButton.background;
  2951. };
  2952. });
  2953.  
  2954. currentDeleteButtonStyles = {
  2955. background: theme.styles.deleteButton.background,
  2956. color: theme.styles.deleteButton.color,
  2957. hoverBackground: theme.styles.deleteButton.hoverBackground
  2958. };
  2959. console.log('[7BTV Control Emotes Panel] Сохранены стили кнопки Delete:', currentDeleteButtonStyles);
  2960.  
  2961. if (list) {
  2962. const listItemTexts = list.querySelectorAll('.list-item-text');
  2963. const listItemLinks = list.querySelectorAll('.list-item-link');
  2964. const listItemDates = list.querySelectorAll('.list-item-date');
  2965. listItemTexts.forEach(span => {
  2966. Object.assign(span.style, theme.styles.listItemText);
  2967. });
  2968. listItemLinks.forEach(span => {
  2969. Object.assign(span.style, theme.styles.listItemLink);
  2970. });
  2971. listItemDates.forEach(span => {
  2972. Object.assign(span.style, theme.styles.listItemDate);
  2973. });
  2974. }
  2975.  
  2976. saveSelectedTheme(themeName);
  2977. }
  2978.  
  2979. // Функция для обновления селектора тем
  2980. function updateThemeSelector() {
  2981. themeSelect.innerHTML = ''; // Очищаем текущие опции
  2982. themes.forEach(theme => {
  2983. const option = document.createElement('option');
  2984. option.value = theme.name;
  2985. option.innerText = theme.displayName;
  2986. if (theme.name === selectedThemeName) {
  2987. option.selected = true;
  2988. }
  2989. themeSelect.appendChild(option);
  2990. });
  2991. }
  2992.  
  2993. // Инициализация интерфейса выбора тем
  2994. const themeSelectorContainer = document.createElement('div');
  2995. themeSelectorContainer.style.position = 'relative';
  2996. themeSelectorContainer.style.bottom = '100px';
  2997. themeSelectorContainer.style.width = '126px';
  2998. themeSelectorContainer.style.left = '0px';
  2999. themeSelectorContainer.style.zIndex = '10001';
  3000.  
  3001. const themeSelect = document.createElement('select');
  3002. themeSelect.style.padding = '5px';
  3003. themeSelect.style.height = '35px';
  3004. themeSelect.style.width = '126px';
  3005. themeSelect.style.borderRadius = '4px';
  3006. themeSelect.style.background = '#192427';
  3007. themeSelect.style.color = '#b69dcf';
  3008. themeSelect.style.border = '1px solid #b69dcf';
  3009.  
  3010. themeSelect.onchange = () => {
  3011. const selectedTheme = themeSelect.value;
  3012. applyTheme(selectedTheme);
  3013. };
  3014.  
  3015. themeSelectorContainer.appendChild(themeSelect);
  3016. controlPanel.appendChild(themeSelectorContainer);
  3017.  
  3018. // Загружаем темы с GitHub и инициализируем интерфейс
  3019. loadThemesFromGitHub()
  3020. .then(() => {
  3021. updateThemeSelector();
  3022. applyTheme(selectedThemeName);
  3023. })
  3024. .catch(() => {
  3025. console.warn('[7BTV Control Emotes Panel] Не удалось загрузить темы с GitHub, использую кэшированные или резервные');
  3026. updateThemeSelector();
  3027. applyTheme(selectedThemeName);
  3028. });
  3029.  
  3030.  
  3031. // Запускаем watchdog
  3032. startWatchdog();
  3033. })();
  3034.  
  3035. })()})();