您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
// ==UserScript== // @name Grok Code Filter Menu 1.21.18 // @namespace http://tampermonkey.net/ // @version 1.21.18 // @description Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек // @author tapeavion // @license MIT // @match https://grok.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @downloadURL // @updateURL // ==/UserScript== (function() { 'use strict'; // Стили const style = document.createElement('style'); style.textContent = ` .filter-menu-btn { position: absolute; top: 4px; right: 460px; height: 31px !important; z-index: 1; padding: 4px 8px; background: #1d5752; color: #b9bcc1; border: none; border-radius: 8px; cursor: pointer; font-size: 12px; transition: background 0.2s ease, color 0.2s ease; } .filter-menu-btn:hover { background: #4a8983; color: #ffffff; } .filter-menu { position: absolute; top: 40px; right: 10px; background: #2d2d2d; border: 1px solid #444; border-radius: 8px; padding: 5px; z-index: 9999; display: none; box-shadow: 0 2px 4px rgba(0,0,0,0.3); width: 200px; max-height: 550px; overflow-y: auto; } .filter-item { display: flex; align-items: center; padding: 5px 0; color: #a0a0a0; font-size: 12px; } .filter-item input[type="checkbox"] { margin-right: 5px; } .filter-item label { flex: 1; cursor: pointer; } .filter-slider { display: none; margin: 5px 0 5px 20px; width: calc(100% - 20px); } .filter-slider-label { display: none; color: #a0a0a0; font-size: 12px; margin: 2px 0 2px 20px; } .language-select { width: 100%; padding: 5px; margin-bottom: 5px; background: #3a3a3a; color: #a0a0a0; border: none; border-radius: 4px; font-size: 12px; } .color-picker { margin: 5px 0 5px 20px; width: calc(100% - 20px); } .color-picker-label { display: block; color: #a0a0a0; font-size: 12px; margin: 2px 0 2px 20px; } button.inline-flex { background-color: #1d5752 !important; opacity: 0; animation: fadeIn 1s ease-in-out forwards; } button.inline-flex:hover { background-color: #1d5752 !important; opacity: 1; } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } } `; document.head.appendChild(style); // Определение языка пользователя const userLang = navigator.language || navigator.languages[0]; const isRussian = userLang.startsWith('ru'); const defaultLang = isRussian ? 'ru' : 'en'; const savedLang = localStorage.getItem('filterMenuLang') || defaultLang; // Локализация const translations = { ru: { filtersBtn: 'Фильтры', sliderLabel: 'Степень:', commentColorLabel: 'Цвет комментариев:', filters: [ { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' }, { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 }, { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 }, { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' }, { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 }, { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 } ], langSelect: 'Выберите язык:', langOptions: [ { value: 'ru', label: 'Русский' }, { value: 'en', label: 'English' } ] }, en: { filtersBtn: 'Filters', sliderLabel: 'Level:', commentColorLabel: 'Comment color:', filters: [ { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }, { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' }, { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 }, { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 }, { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' }, { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 }, { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 } ], langSelect: 'Select language:', langOptions: [ { value: 'ru', label: 'Русский' }, { value: 'en', label: 'English' } ] } }; // Глобальная переменная для текущего цвета комментариев let currentCommentColor = localStorage.getItem('commentColor') || '#5c6370'; // Массив для хранения всех пар headerBlock и codeContainer const codeBlockRegistry = []; // Функция создания меню фильтров function addFilterMenu(headerBlock, codeContainer) { if (headerBlock.querySelector('.filter-menu-btn')) { console.log('Фильтр уже существует для заголовка:', headerBlock); return; } // Сохраняем пару headerBlock и codeContainer codeBlockRegistry.push({ headerBlock, codeContainer }); console.log('Добавлен кодовый блок в реестр:', codeContainer.outerHTML); let currentLang = savedLang; const filterBtn = document.createElement('button'); filterBtn.className = 'filter-menu-btn'; filterBtn.textContent = translations[currentLang].filtersBtn; const filterMenu = document.createElement('div'); filterMenu.className = 'filter-menu'; // Целевой блок — контейнер кода const targetBlock = codeContainer; // Загружаем сохраненные настройки const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}'); const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}'); // Инициализируем значения по умолчанию const filters = translations[currentLang].filters; filters.forEach(filter => { if (!(filter.value in savedFilterStates)) { savedFilterStates[filter.value] = false; } if (!(filter.value in savedFilterValues)) { savedFilterValues[filter.value] = filter.default; } }); // Применяем сохраненные фильтры function applyFilters() { const activeFilters = filters .filter(filter => savedFilterStates[filter.value]) .map(filter => { const unit = filter.unit || ''; const value = savedFilterValues[filter.value]; return `${filter.value}(${value}${unit})`; }); targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none'; console.log('Применены фильтры к контейнеру:', targetBlock, activeFilters); } // Применяем цвет комментариев // Альтернативный подход: применение цвета через глобальный стиль function applyGlobalCommentColor() { const existingStyle = document.getElementById('custom-comment-style'); if (existingStyle) existingStyle.remove(); const style = document.createElement('style'); style.id = 'custom-comment-style'; style.textContent = ` .hljs-comment, span[style*="color: rgb(92, 99, 112)"] { color: ${currentCommentColor} !important; } `; document.head.appendChild(style); console.log('Применен глобальный стиль для комментариев:', currentCommentColor); } applyFilters(); applyGlobalCommentColor(); // Создаем выпадающий список для выбора языка const langSelect = document.createElement('select'); langSelect.className = 'language-select'; const langLabel = document.createElement('label'); langLabel.textContent = translations[currentLang].langSelect; langLabel.style.color = '#a0a0a0'; langLabel.style.fontSize = '12px'; langLabel.style.marginBottom = '2px'; langLabel.style.display = 'block'; translations[currentLang].langOptions.forEach(option => { const opt = document.createElement('option'); opt.value = option.value; opt.textContent = option.label; if (option.value === currentLang) { opt.selected = true; } langSelect.appendChild(opt); }); // Создаем элемент для выбора цвета комментариев const colorPickerLabel = document.createElement('label'); colorPickerLabel.className = 'color-picker-label'; colorPickerLabel.textContent = translations[currentLang].commentColorLabel; const colorPicker = document.createElement('input'); colorPicker.type = 'color'; colorPicker.className = 'color-picker'; colorPicker.value = currentCommentColor; colorPicker.addEventListener('input', () => { currentCommentColor = colorPicker.value; localStorage.setItem('commentColor', currentCommentColor); console.log('Изменен цвет комментариев:', currentCommentColor); refreshAllCodeBlocks(); }); // Функция обновления интерфейса при смене языка function updateLanguage(lang) { currentLang = lang; localStorage.setItem('filterMenuLang', currentLang); filterBtn.textContent = translations[currentLang].filtersBtn; langLabel.textContent = translations[currentLang].langSelect; colorPickerLabel.textContent = translations[currentLang].commentColorLabel; filterMenu.innerHTML = ''; filterMenu.appendChild(langLabel); filterMenu.appendChild(langSelect); filterMenu.appendChild(colorPickerLabel); filterMenu.appendChild(colorPicker); renderFilters(); } // Обработчик смены языка langSelect.addEventListener('change', () => { updateLanguage(langSelect.value); }); // Рендеринг фильтров function renderFilters() { const filters = translations[currentLang].filters; filters.forEach(filter => { const filterItem = document.createElement('div'); filterItem.className = 'filter-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = savedFilterStates[filter.value]; checkbox.id = `filter-${filter.value}`; const label = document.createElement('label'); label.htmlFor = `filter-${filter.value}`; label.textContent = filter.name; const sliderLabel = document.createElement('label'); sliderLabel.className = 'filter-slider-label'; sliderLabel.textContent = translations[currentLang].sliderLabel; const slider = document.createElement('input'); slider.type = 'range'; slider.className = 'filter-slider'; slider.min = filter.min; slider.max = filter.max; slider.step = filter.step; slider.value = savedFilterValues[filter.value]; if (checkbox.checked && filter.hasSlider) { slider.style.display = 'block'; sliderLabel.style.display = 'block'; } checkbox.addEventListener('change', () => { savedFilterStates[filter.value] = checkbox.checked; localStorage.setItem('codeFilterStates', JSON.stringify(savedFilterStates)); if (filter.hasSlider) { slider.style.display = checkbox.checked ? 'block' : 'none'; sliderLabel.style.display = checkbox.checked ? 'block' : 'none'; } console.log('Изменен фильтр:', filter.value, checkbox.checked); refreshAllCodeBlocks(); }); slider.addEventListener('input', () => { savedFilterValues[filter.value] = slider.value; localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues)); console.log('Изменено значение фильтра:', filter.value, slider.value); refreshAllCodeBlocks(); }); filterItem.appendChild(checkbox); filterItem.appendChild(label); filterMenu.appendChild(filterItem); filterMenu.appendChild(sliderLabel); filterMenu.appendChild(slider); }); } // Инициализация filterMenu.appendChild(langLabel); filterMenu.appendChild(langSelect); filterMenu.appendChild(colorPickerLabel); filterMenu.appendChild(colorPicker); renderFilters(); // Обработчики для кнопки filterBtn.addEventListener('click', () => { filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block'; }); document.addEventListener('click', (e) => { if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) { filterMenu.style.display = 'none'; } }); headerBlock.style.position = 'relative'; headerBlock.appendChild(filterBtn); headerBlock.appendChild(filterMenu); } // Функция логирования структуры DOM для отладки function logDomStructure(headerBlock) { console.log('Заголовок блока кода:', headerBlock.outerHTML); console.log('Следующий элемент (nextElementSibling):', headerBlock.nextElementSibling?.outerHTML || 'Не найден'); console.log('Родительский элемент:', headerBlock.parentElement.outerHTML); console.log('Все <code> в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('code')).map(el => el.outerHTML)); console.log('Все .not-prose в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('.not-prose')).map(el => el.outerHTML)); } // Функция для управления селекторами и контейнерами function manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer) { const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}'); // Сохраняем успешные селекторы и контейнеры if (headerBlocks.length > 0 && codeContainer) { savedSelectors[headerSelectorUsed] = savedSelectors[headerSelectorUsed] || {}; savedSelectors[headerSelectorUsed].headerCount = headerBlocks.length; savedSelectors[headerSelectorUsed].containerSelector = containerSelectorUsed; savedSelectors[headerSelectorUsed].lastUsed = Date.now(); localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors)); console.log('Сохранены селекторы:', headerSelectorUsed, containerSelectorUsed); } // Очистка устаревших селекторов (старше 7 дней) const oneWeek = 7 * 24 * 60 * 60 * 1000; Object.keys(savedSelectors).forEach(selector => { if (Date.now() - savedSelectors[selector].lastUsed > oneWeek) { delete savedSelectors[selector]; } }); localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors)); return savedSelectors; } // Функция для обновления всех кодовых блоков function refreshAllCodeBlocks() { const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}'); const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}'); const filters = [ { value: 'invert', default: 1 }, { value: 'sepia', default: 1 }, { value: 'grayscale', default: 1 }, { value: 'blur', unit: 'px', default: 2 }, { value: 'contrast', default: 2 }, { value: 'brightness', default: 1.5 }, { value: 'hue-rotate', unit: 'deg', default: 90 }, { value: 'saturate', default: 1 }, { value: 'opacity', default: 1 } ]; // Применяем к зарегистрированным блокам console.log('Реестр кодовых блоков:', codeBlockRegistry.length); codeBlockRegistry.forEach(({ codeContainer }) => { const activeFilters = filters .filter(filter => savedFilterStates[filter.value]) .map(filter => { const unit = filter.unit || ''; const value = savedFilterValues[filter.value] || filter.default; return `${filter.value}(${value}${unit})`; }); codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none'; console.log('Применены фильтры к зарегистрированному контейнеру:', codeContainer, activeFilters); const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment'); console.log('Найдено комментариев в зарегистрированном контейнере:', commentElements.length); commentElements.forEach(element => { element.style.color = currentCommentColor; }); }); // Глобальное применение к новым или незарегистрированным блокам const allCodeContainers = document.querySelectorAll('.not-prose div > code, .not-prose pre > code'); allCodeContainers.forEach(codeContainer => { if (!codeBlockRegistry.some(reg => reg.codeContainer === codeContainer.parentElement)) { const activeFilters = filters .filter(filter => savedFilterStates[filter.value]) .map(filter => { const unit = filter.unit || ''; const value = savedFilterValues[filter.value] || filter.default; return `${filter.value}(${value}${unit})`; }); codeContainer.parentElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none'; console.log('Применены фильтры к глобальному .not-prose контейнеру:', codeContainer.parentElement, activeFilters); const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment'); console.log('Найдено комментариев в глобальном .not-prose контейнере:', commentElements.length); commentElements.forEach(element => { element.style.color = currentCommentColor; }); } }); } // Функция поиска и обработки блоков кода function processCodeBlocks() { // Селекторы для заголовков блоков кода const headerSelectors = [ 'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs', 'div[class*="flex"][class*="bg-surface"] > span', 'div > span[class*="font-mono"]' ]; // Селекторы для контейнеров кода const containerSelectors = [ 'nextElementSibling', '.not-prose div[style*="overflow-x: auto"]', '.not-prose div > code', '.not-prose pre > code', '.not-prose div[style*="background: hsl"]' ]; // Загружаем сохраненные селекторы const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}'); let headerBlocks = []; let headerSelectorUsed = null; let containerSelectorUsed = null; let codeContainer = null; // Сначала пытаемся использовать сохраненные селекторы for (const savedSelector of Object.keys(savedSelectors)) { const headers = Array.from(document.querySelectorAll(savedSelector)) .filter(span => { const text = span.textContent.toLowerCase(); return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text); }) .map(span => span.closest('div')); if (headers.length > 0) { headerBlocks = [...new Set(headers)]; headerSelectorUsed = savedSelector; const savedContainerSelector = savedSelectors[savedSelector].containerSelector; // Проверяем сохраненный селектор контейнера for (const headerBlock of headers) { if (savedContainerSelector === 'nextElementSibling') { codeContainer = headerBlock.nextElementSibling?.querySelector('code') ? headerBlock.nextElementSibling : null; } else if (savedContainerSelector === '.not-prose div > code') { codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement; } else if (savedContainerSelector === '.not-prose pre > code') { codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement; } else { codeContainer = headerBlock.parentElement.querySelector(savedContainerSelector); } if (codeContainer) { containerSelectorUsed = savedContainerSelector; console.log('Использованы сохраненные селекторы:', headerSelectorUsed, containerSelectorUsed); break; } } if (codeContainer) break; } } // Если сохраненные селекторы не сработали, ищем новые if (headerBlocks.length === 0) { for (const selector of headerSelectors) { const headers = Array.from(document.querySelectorAll(selector)) .filter(span => { const text = span.textContent.toLowerCase(); return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text); }) .map(span => span.closest('div')); if (headers.length > 0) { headerBlocks = [...new Set(headers)]; headerSelectorUsed = selector; break; } } } console.log('Найдено заголовков блоков кода:', headerBlocks.length); headerBlocks.forEach(headerBlock => { const langSpan = headerBlock.querySelector('span.font-mono.text-xs'); if (!langSpan) { console.log('Заголовок без span с языком:', headerBlock); return; } // Пытаемся найти контейнер кода codeContainer = null; if (headerBlock.nextElementSibling?.querySelector('code')) { codeContainer = headerBlock.nextElementSibling; containerSelectorUsed = 'nextElementSibling'; } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]')) { codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]'); containerSelectorUsed = '.not-prose div[style*="overflow-x: auto"]'; } else if (headerBlock.parentElement.querySelector('.not-prose div > code')) { codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement; containerSelectorUsed = '.not-prose div > code'; } else if (headerBlock.parentElement.querySelector('.not-prose pre > code')) { codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement; containerSelectorUsed = '.not-prose pre > code'; } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]')) { codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]'); containerSelectorUsed = '.not-prose div[style*="background: hsl"]'; } if (codeContainer) { console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML); addFilterMenu(headerBlock, codeContainer); // Сохраняем успешные селекторы manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer); } else { console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML); logDomStructure(headerBlock); } }); // Обновляем все кодовые блоки после обработки refreshAllCodeBlocks(); } // Инициализация setTimeout(() => { console.log('Инициализация processCodeBlocks'); processCodeBlocks(); }, 2000); // Наблюдатель за изменениями DOM const observer = new MutationObserver((mutations) => { console.log('Обнаружены изменения DOM, вызывается processCodeBlocks'); processCodeBlocks(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); // Инициализируем глобальное применение фильтров refreshAllCodeBlocks(); })();