Grok Code Filter Menu

Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек

当前为 2025-03-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Grok Code Filter Menu
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.21.11
  5. // @description Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
  6. // @author tapeavion
  7. // @license MIT
  8. // @match https://grok.com/chat/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Стили
  19. GM_addStyle(`
  20. .filter-menu-btn {
  21. position: absolute;
  22. top: 4px;
  23. right: 345px;
  24. z-index: 9999;
  25. padding: 4px 8px;
  26. background: #2d2d2d;
  27. color: #a0a0a0;
  28. border: none;
  29. border-radius: 8px;
  30. cursor: pointer;
  31. font-size: 12px;
  32. transition: background 0.2s ease, color 0.2s ease;
  33. }
  34. .filter-menu-btn:hover {
  35. background: #4a4a4a;
  36. color: #ffffff;
  37. }
  38. .filter-menu {
  39. position: absolute;
  40. top: 32px;
  41. right: 160px;
  42. background: #2d2d2d;
  43. border: 1px solid #444;
  44. border-radius: 8px;
  45. padding: 5px;
  46. z-index: 9999;
  47. display: none;
  48. box-shadow: 0 2px 4px rgba(0,0,0,0.3);
  49. width: 200px;
  50. max-height: 400px;
  51. overflow-y: auto;
  52. }
  53. .filter-item {
  54. display: flex;
  55. align-items: center;
  56. padding: 5px 0;
  57. color: #a0a0a0;
  58. font-size: 12px;
  59. }
  60. .filter-item input[type="checkbox"] {
  61. margin-right: 5px;
  62. }
  63. .filter-item label {
  64. flex: 1;
  65. cursor: pointer;
  66. }
  67. .filter-slider {
  68. display: none;
  69. margin: 5px 0 5px 20px;
  70. width: calc(100% - 20px);
  71. }
  72. .filter-slider-label {
  73. display: none;
  74. color: #a0a0a0;
  75. font-size: 12px;
  76. margin: 2px 0 2px 20px;
  77. }
  78. .language-select {
  79. width: 100%;
  80. padding: 5px;
  81. margin-bottom: 5px;
  82. background: #3a3a3a;
  83. color: #a0a0a0;
  84. border: none;
  85. border-radius: 4px;
  86. font-size: 12px;
  87. }
  88. .color-picker {
  89. margin: 5px 0 5px 20px;
  90. width: calc(100% - 20px);
  91. }
  92. .color-picker-label {
  93. display: block;
  94. color: #a0a0a0;
  95. font-size: 12px;
  96. margin: 2px 0 2px 20px;
  97. }
  98. `);
  99.  
  100. // Определение языка пользователя
  101. const userLang = navigator.language || navigator.languages[0];
  102. const isRussian = userLang.startsWith('ru');
  103. const defaultLang = isRussian ? 'ru' : 'en';
  104. const savedLang = GM_getValue('filterMenuLang', defaultLang);
  105.  
  106. // Локализация
  107. const translations = {
  108. ru: {
  109. filtersBtn: 'Фильтры',
  110. sliderLabel: 'Степень:',
  111. commentColorLabel: 'Цвет комментариев:',
  112. filters: [
  113. { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  114. { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  115. { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  116. { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  117. { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  118. { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  119. { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  120. { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  121. { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  122. ],
  123. langSelect: 'Выберите язык:',
  124. langOptions: [
  125. { value: 'ru', label: 'Русский' },
  126. { value: 'en', label: 'English' }
  127. ]
  128. },
  129. en: {
  130. filtersBtn: 'Filters',
  131. sliderLabel: 'Level:',
  132. commentColorLabel: 'Comment color:',
  133. filters: [
  134. { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  135. { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  136. { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  137. { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  138. { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  139. { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  140. { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  141. { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  142. { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  143. ],
  144. langSelect: 'Select language:',
  145. langOptions: [
  146. { value: 'ru', label: 'Русский' },
  147. { value: 'en', label: 'English' }
  148. ]
  149. }
  150. };
  151.  
  152. // Глобальная переменная для текущего цвета комментариев
  153. let currentCommentColor = GM_getValue('commentColor', '#5c6370');
  154.  
  155. // Функция создания меню фильтров
  156. function addFilterMenu(codeBlock) {
  157. if (codeBlock.querySelector('.filter-menu-btn')) return;
  158.  
  159. let currentLang = savedLang;
  160. const filterBtn = document.createElement('button');
  161. filterBtn.className = 'filter-menu-btn';
  162. filterBtn.textContent = translations[currentLang].filtersBtn;
  163.  
  164. const filterMenu = document.createElement('div');
  165. filterMenu.className = 'filter-menu';
  166.  
  167. const targetBlock = codeBlock.querySelector('div[style*="background: rgb(18, 19, 20)"]') || codeBlock;
  168.  
  169. // Загружаем сохраненные настройки
  170. const savedFilterStates = GM_getValue('codeFilterStates', {});
  171. const savedFilterValues = GM_getValue('codeFilterValues', {});
  172.  
  173. // Инициализируем значения по умолчанию
  174. const filters = translations[currentLang].filters;
  175. filters.forEach(filter => {
  176. if (!(filter.value in savedFilterStates)) {
  177. savedFilterStates[filter.value] = false;
  178. }
  179. if (!(filter.value in savedFilterValues)) {
  180. savedFilterValues[filter.value] = filter.default;
  181. }
  182. });
  183.  
  184. // Применяем сохраненные фильтры
  185. function applyFilters() {
  186. const activeFilters = filters
  187. .filter(filter => savedFilterStates[filter.value])
  188. .map(filter => {
  189. const unit = filter.unit || '';
  190. const value = savedFilterValues[filter.value];
  191. return `${filter.value}(${value}${unit})`;
  192. });
  193. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  194. }
  195.  
  196. // Применяем цвет комментариев
  197. function applyCommentColor() {
  198. const commentElements = codeBlock.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
  199. commentElements.forEach(element => {
  200. element.style.color = currentCommentColor;
  201. });
  202. }
  203. applyFilters();
  204. applyCommentColor();
  205.  
  206. // Создаем выпадающий список для выбора языка
  207. const langSelect = document.createElement('select');
  208. langSelect.className = 'language-select';
  209. const langLabel = document.createElement('label');
  210. langLabel.textContent = translations[currentLang].langSelect;
  211. langLabel.style.color = '#a0a0a0';
  212. langLabel.style.fontSize = '12px';
  213. langLabel.style.marginBottom = '2px';
  214. langLabel.style.display = 'block';
  215.  
  216. translations[currentLang].langOptions.forEach(option => {
  217. const opt = document.createElement('option');
  218. opt.value = option.value;
  219. opt.textContent = option.label;
  220. if (option.value === currentLang) {
  221. opt.selected = true;
  222. }
  223. langSelect.appendChild(opt);
  224. });
  225.  
  226. // Создаем элемент для выбора цвета комментариев
  227. const colorPickerLabel = document.createElement('label');
  228. colorPickerLabel.className = 'color-picker-label';
  229. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  230.  
  231. const colorPicker = document.createElement('input');
  232. colorPicker.type = 'color';
  233. colorPicker.className = 'color-picker';
  234. colorPicker.value = currentCommentColor;
  235.  
  236. colorPicker.addEventListener('input', () => {
  237. currentCommentColor = colorPicker.value;
  238. GM_setValue('commentColor', currentCommentColor);
  239. // Применяем цвет ко всем блокам на странице
  240. document.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]').forEach(element => {
  241. element.style.color = currentCommentColor;
  242. });
  243. });
  244.  
  245. // Функция обновления интерфейса при смене языка
  246. function updateLanguage(lang) {
  247. currentLang = lang;
  248. GM_setValue('filterMenuLang', currentLang);
  249. filterBtn.textContent = translations[currentLang].filtersBtn;
  250. langLabel.textContent = translations[currentLang].langSelect;
  251. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  252. filterMenu.innerHTML = ''; // Очищаем меню
  253. filterMenu.appendChild(langLabel);
  254. filterMenu.appendChild(langSelect);
  255. filterMenu.appendChild(colorPickerLabel);
  256. filterMenu.appendChild(colorPicker);
  257. renderFilters();
  258. }
  259.  
  260. // Обработчик смены языка
  261. langSelect.addEventListener('change', () => {
  262. updateLanguage(langSelect.value);
  263. });
  264.  
  265. // Рендеринг фильтров
  266. function renderFilters() {
  267. const filters = translations[currentLang].filters;
  268. filters.forEach(filter => {
  269. const filterItem = document.createElement('div');
  270. filterItem.className = 'filter-item';
  271.  
  272. const checkbox = document.createElement('input');
  273. checkbox.type = 'checkbox';
  274. checkbox.checked = savedFilterStates[filter.value];
  275. checkbox.id = `filter-${filter.value}`;
  276.  
  277. const label = document.createElement('label');
  278. label.htmlFor = `filter-${filter.value}`;
  279. label.textContent = filter.name;
  280.  
  281. const sliderLabel = document.createElement('label');
  282. sliderLabel.className = 'filter-slider-label';
  283. sliderLabel.textContent = translations[currentLang].sliderLabel;
  284.  
  285. const slider = document.createElement('input');
  286. slider.type = 'range';
  287. slider.className = 'filter-slider';
  288. slider.min = filter.min;
  289. slider.max = filter.max;
  290. slider.step = filter.step;
  291. slider.value = savedFilterValues[filter.value];
  292.  
  293. if (checkbox.checked && filter.hasSlider) {
  294. slider.style.display = 'block';
  295. sliderLabel.style.display = 'block';
  296. }
  297.  
  298. checkbox.addEventListener('change', () => {
  299. savedFilterStates[filter.value] = checkbox.checked;
  300. GM_setValue('codeFilterStates', savedFilterStates);
  301. if (filter.hasSlider) {
  302. slider.style.display = checkbox.checked ? 'block' : 'none';
  303. sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
  304. }
  305. applyFilters();
  306. });
  307.  
  308. slider.addEventListener('input', () => {
  309. savedFilterValues[filter.value] = slider.value;
  310. GM_setValue('codeFilterValues', savedFilterValues);
  311. applyFilters();
  312. });
  313.  
  314. filterItem.appendChild(checkbox);
  315. filterItem.appendChild(label);
  316. filterMenu.appendChild(filterItem);
  317. filterMenu.appendChild(sliderLabel);
  318. filterMenu.appendChild(slider);
  319. });
  320. }
  321.  
  322. // Инициализация
  323. filterMenu.appendChild(langLabel);
  324. filterMenu.appendChild(langSelect);
  325. filterMenu.appendChild(colorPickerLabel);
  326. filterMenu.appendChild(colorPicker);
  327. renderFilters();
  328.  
  329. // Обработчики для кнопки
  330. filterBtn.addEventListener('click', () => {
  331. filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
  332. });
  333.  
  334. document.addEventListener('click', (e) => {
  335. if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
  336. filterMenu.style.display = 'none';
  337. }
  338. });
  339.  
  340. codeBlock.style.position = 'relative';
  341. codeBlock.appendChild(filterBtn);
  342. codeBlock.appendChild(filterMenu);
  343. }
  344.  
  345. // Функция поиска и обработки блоков кода
  346. function processCodeBlocks() {
  347. const codeBlocks = document.querySelectorAll('div[class*="relative"][class*="mt-3"][class*="mb-3"][class*="-mx-4"]');
  348. console.log('Найдено блоков кода:', codeBlocks.length);
  349. codeBlocks.forEach(block => {
  350. if (block.querySelector('div[style*="background: rgb(18, 19, 20)"]')) {
  351. addFilterMenu(block);
  352. const savedFilterStates = GM_getValue('codeFilterStates', {});
  353. const savedFilterValues = GM_getValue('codeFilterValues', {});
  354. const targetBlock = block.querySelector('div[style*="background: rgb(18, 19, 20)"]') || block;
  355. const filters = [
  356. { value: 'invert' },
  357. { value: 'sepia' },
  358. { value: 'grayscale' },
  359. { value: 'blur', unit: 'px' },
  360. { value: 'contrast' },
  361. { value: 'brightness' },
  362. { value: 'hue-rotate', unit: 'deg' },
  363. { value: 'saturate' },
  364. { value: 'opacity' }
  365. ];
  366. const activeFilters = filters
  367. .filter(filter => savedFilterStates[filter.value])
  368. .map(filter => {
  369. const unit = filter.unit || '';
  370. const value = savedFilterValues[filter.value] || (filter.value === 'blur' ? 2 : filter.value === 'brightness' ? 1.5 : filter.value === 'contrast' ? 2 : filter.value === 'hue-rotate' ? 90 : 1);
  371. return `${filter.value}(${value}${unit})`;
  372. });
  373. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  374. const commentElements = block.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
  375. commentElements.forEach(element => {
  376. element.style.color = currentCommentColor;
  377. });
  378. }
  379. });
  380. }
  381.  
  382. setTimeout(processCodeBlocks, 1000);
  383. processCodeBlocks();
  384.  
  385. const observer = new MutationObserver(() => {
  386. processCodeBlocks();
  387. });
  388. observer.observe(document.body, { childList: true, subtree: true });
  389. })();

QingJ © 2025

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