您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Виджет для сбора ID уроков и тренингов на страницах GetCourse с адаптивной контрастностью
当前为
// ==UserScript== // @name GetCourse ID Collector // @namespace https://dev-postnov.ru/ // @version 3.1.0 // @description Виджет для сбора ID уроков и тренингов на страницах GetCourse с адаптивной контрастностью // @author Daniil Postnov // @match *://*/teach/control/stream/* // @match *://*/teach/control/* // @match *://*/teach/* // @match *://*/teach // @match *://*/teach/control // @grant GM_setClipboard // @grant GM_notification // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function() { 'use strict'; // ====== ФУНКЦИИ ДЛЯ КОНТРАСТНОЙ ПОДСТАНОВКИ ЦВЕТА ====== // Определяем яркость цвета (0...255) на основе R,G,B function getLuminance(colorStr) { // Ищем в формате rgb(...) / rgba(...) const rgbMatch = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!rgbMatch) { // Если фон не найден (transparent и т.д.), возвращаем белую яркость return 255; } const r = parseInt(rgbMatch[1], 10); const g = parseInt(rgbMatch[2], 10); const b = parseInt(rgbMatch[3], 10); return 0.299 * r + 0.587 * g + 0.114 * b; } // Возвращаем пару (bg, text) для контрастной плашки function getContrastingColors(parentBgColor) { const lum = getLuminance(parentBgColor); const lightBg = '#F0F0F0'; const lightText = '#000000'; const darkBg = '#333333'; const darkText = '#FFFFFF'; // Выбираем «тёмная плашка» при светлом фоне, и наоборот if (lum > 127) { return { bg: darkBg, text: darkText }; } else { return { bg: lightBg, text: lightText }; } } // ====== DEBOUNCE ====== function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // ====== СЕЛЕКТОРЫ И ОБЩИЕ ПЕРЕМЕННЫЕ ====== const LESSON_SELECTORS = [ '.lesson-list li a', '.lesson-is-hidden', '[onclick*="/teach/control/lesson/view/id/"]' ].join(', '); // Получение настроек const idWrapSymbol = GM_getValue('idWrapSymbol', "'"); let isUpdating = false; // флаг для защиты от рекурсии // ====== EXTRACT ID ====== function extractId(element) { let id = null; if (element.href) { const match = element.href.match(/\/id\/(\d+)/); if (match) id = match[1]; } if (!id && element.getAttribute('onclick')) { const match = element.getAttribute('onclick').match(/\/id\/(\d+)/); if (match) id = match[1]; } return id; } // Функция для форматирования ID согласно настройкам function formatId(id) { if (!id) return null; const symbol = GM_getValue('idWrapSymbol', ''); return symbol ? `${symbol}${id}${symbol}` : id; } // ====== ДОБАВЛЕНИЕ МЕТКИ ID ====== function createIdLabel(id, parentElement) { const label = document.createElement('span'); label.className = 'id-label'; label.textContent = `ID: ${id}`; label.title = 'Нажмите, чтобы скопировать'; // Определяем цвет фона родителя и выставляем контрастный const computedStyle = window.getComputedStyle(parentElement); const bgColorParent = computedStyle.backgroundColor; const { bg, text } = getContrastingColors(bgColorParent); label.style.backgroundColor = bg; label.style.color = text; // Клик по плашке (копирование) label.addEventListener('click', () => { GM_setClipboard(formatId(id)); label.textContent = 'Скопировано!'; setTimeout(() => { label.textContent = `ID: ${id}`; }, 1000); }); return label; } // ====== ПОКАЗ ID ====== function showIds() { if (isUpdating) return; isUpdating = true; try { // Удаляем старые плашки document.querySelectorAll('.id-label').forEach(label => label.remove()); // ТРЕНИНГИ document.querySelectorAll('.training-row a').forEach(link => { const id = extractId(link); if (id) { const td = link.closest('td'); if (td) { const label = createIdLabel(id, td); td.appendChild(label); } } }); // УРОКИ document.querySelectorAll(LESSON_SELECTORS).forEach(element => { const id = extractId(element); if (id) { // 1) Пытаемся найти <td> let container = element.closest('td'); // 2) Если нет <td>, пробуем найти <li> if (!container) { container = element.closest('li'); } // 3) Если и <li> нет, как fallback берём родителя if (!container) { container = element.parentElement; } if (container) { const label = createIdLabel(id, container); container.appendChild(label); } } }); } catch (error) { console.error('Error in showIds:', error); GM_notification('Произошла ошибка при обновлении ID', 'GetCourse Widget'); } finally { isUpdating = false; } } // ====== ОБРАБОТЧИК MUTATION OBSERVER ====== const observer = new MutationObserver( debounce((mutations) => { const hasRelevantChanges = mutations.some(mutation => { return Array.from(mutation.addedNodes).some(node => { if (node.nodeType !== 1) return false; const isOurElement = node.classList?.contains('id-label') || node.classList?.contains('get-id-widget-panel') || node.classList?.contains('get-id-widget-tooltip') || node.classList?.contains('get-id-widget-settings'); return !isOurElement; }); }); if (hasRelevantChanges && !isUpdating) { if (!document.querySelector('.get-id-widget-panel')) { addWidget(); } showIds(); } }, 100) ); // ====== НАСТРОЙКИ ВИДЖЕТА ====== function showSettings() { // Удаляем существующую панель настроек если есть const existingSettings = document.querySelector('.get-id-widget-settings'); if (existingSettings) { existingSettings.remove(); return; } // Создаем панель настроек const settings = document.createElement('div'); settings.className = 'get-id-widget-settings'; // Текущее значение const currentSymbol = GM_getValue('idWrapSymbol', ''); // HTML для настроек settings.innerHTML = ` <div class="settings-header">Настройки</div> <div class="settings-row"> <label for="id-wrap-symbol">Символ обрамления ID:</label> <input type="text" id="id-wrap-symbol" value="${currentSymbol}" placeholder="Например: ' или " или пусто"> </div> <div class="settings-info"> Оставьте поле пустым, чтобы копировать ID без обрамления </div> <div class="settings-buttons"> <button id="save-settings">Сохранить</button> <button id="cancel-settings">Отмена</button> </div> `; document.body.appendChild(settings); // Обработчики для кнопок document.getElementById('save-settings').addEventListener('click', () => { const symbol = document.getElementById('id-wrap-symbol').value; GM_setValue('idWrapSymbol', symbol); GM_notification('Настройки сохранены', 'GetCourse Widget'); settings.remove(); }); document.getElementById('cancel-settings').addEventListener('click', () => { settings.remove(); }); } // ====== ДОБАВЛЕНИЕ ПАНЕЛИ И ТУЛТИПА ====== function addWidget() { if (document.querySelector('.get-id-widget-panel')) return; const panel = document.createElement('div'); panel.className = 'get-id-widget-panel'; const copyButton = document.createElement('button'); const viewButton = document.createElement('button'); const clearButton = document.createElement('button'); const settingsButton = document.createElement('button'); copyButton.textContent = 'Скопировать ID'; viewButton.textContent = 'Посмотреть буфер'; clearButton.textContent = 'Очистить буфер'; settingsButton.textContent = '⚙️ Настройки'; panel.appendChild(copyButton); panel.appendChild(viewButton); panel.appendChild(clearButton); panel.appendChild(settingsButton); document.body.appendChild(panel); // Тултип const tooltip = document.createElement('div'); tooltip.className = 'get-id-widget-tooltip'; document.body.appendChild(tooltip); // Вызываем applyStyles() applyStyles(); // ------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ------- function updateButtonText(button, text, isError = false) { button.textContent = text; button.style.backgroundColor = isError ? '#f44336' : '#fff'; } function getUniqueArray(arr) { return [...new Set(arr)]; } // Парсинг буфера function parseClipboardContent(content) { const result = { trainings: [], lessons: [] }; const trainingsMatch = content.match(/\/\* Тренинги \*\/([\s\S]*?)(?=\/\* Уроки \*\/|$)/); if (trainingsMatch) { const trainingsContent = trainingsMatch[1].trim(); if (trainingsContent) { result.trainings = trainingsContent.split(/,\s*/).filter(id => id.length > 0); } } const lessonsMatch = content.match(/\/\* Уроки \*\/([\s\S]*)/); if (lessonsMatch) { const lessonsContent = lessonsMatch[1].trim(); if (lessonsContent) { result.lessons = lessonsContent.split(/,\s*/).filter(id => id.length > 0); } } return result; } // ------- ОБРАБОТЧИКИ СОБЫТИЙ ------- // Копирование copyButton.addEventListener('click', () => { updateButtonText(copyButton, 'Собираем ID...'); try { const trainingLinks = document.querySelectorAll('.training-row a'); const newTrainingIds = Array.from(trainingLinks) .map(link => extractId(link)) .filter(Boolean) .map(id => formatId(id)); const newLessonIds = Array.from(document.querySelectorAll(LESSON_SELECTORS)) .map(el => extractId(el)) .filter(Boolean) .map(id => formatId(id)); navigator.clipboard.readText() .then(currentText => { const currentIds = parseClipboardContent(currentText); const updatedTrainingIds = getUniqueArray([...currentIds.trainings, ...newTrainingIds]); const updatedLessonIds = getUniqueArray([...currentIds.lessons, ...newLessonIds]); let clipboardText = ''; if (updatedTrainingIds.length > 0) { clipboardText += `/* Тренинги */\n${updatedTrainingIds.join(', ')}`; } if (updatedLessonIds.length > 0) { if (clipboardText) clipboardText += '\n\n'; clipboardText += `/* Уроки */\n${updatedLessonIds.join(', ')}`; } if (!clipboardText) { clipboardText = 'Не найдено ID для копирования'; } GM_setClipboard(clipboardText); GM_notification('ID скопированы!', 'GetCourse Widget'); updateButtonText(copyButton, 'Скопировано! ✅'); setTimeout(() => updateButtonText(copyButton, 'Скопировать ID'), 3000); // Обновим тултип tooltip.innerHTML = clipboardText ? `<div class="tooltip-content">${clipboardText}</div>` : '<div class="tooltip-content">Буфер обмена пуст</div>'; }) .catch(() => { updateButtonText(copyButton, 'Ошибка чтения буфера ❌', true); setTimeout(() => updateButtonText(copyButton, 'Скопировать ID'), 3000); }); } catch (error) { console.error('Error copying IDs:', error); updateButtonText(copyButton, 'Ошибка ❌', true); setTimeout(() => updateButtonText(copyButton, 'Скопировать ID'), 3000); } }); // Очистка буфера clearButton.addEventListener('click', () => { if (confirm('Вы уверены, что хотите очистить буфер обмена?')) { GM_setClipboard(''); GM_notification('Буфер обмена очищен!', 'GetCourse Widget'); updateButtonText(clearButton, 'Буфер очищен! ✅'); setTimeout(() => updateButtonText(clearButton, 'Очистить буфер'), 3000); tooltip.innerHTML = '<div class="tooltip-content">Буфер обмена пуст</div>'; } }); // Просмотр буфера viewButton.addEventListener('click', () => { if (tooltip.classList.contains('show')) { tooltip.classList.remove('show'); tooltip.style.display = 'none'; viewButton.textContent = 'Посмотреть буфер'; } else { navigator.clipboard.readText() .then(bufferContent => { tooltip.innerHTML = bufferContent ? `<div class="tooltip-content">${bufferContent}</div>` : '<div class="tooltip-content">Буфер обмена пуст</div>'; tooltip.classList.add('show'); tooltip.style.display = 'block'; tooltip.style.maxHeight = `${window.innerHeight - 100}px`; tooltip.style.overflowY = 'auto'; viewButton.textContent = 'Закрыть буфер'; }) .catch(() => { tooltip.innerHTML = '<div class="tooltip-content">Ошибка чтения буфера</div>'; tooltip.classList.add('show'); tooltip.style.display = 'block'; viewButton.textContent = 'Закрыть буфер'; }); } }); // Кнопка настроек settingsButton.addEventListener('click', showSettings); } // ====== applyStyles() (CSS-ЧАСТЬ ОПУЩЕНА) ====== // Функция для применения стилей function applyStyles() { const styleElement = document.createElement('style'); styleElement.textContent = ` .lesson-list li { position: relative !important; } /* Панель виджета */ .get-id-widget-panel { position: fixed !important; top: 0 !important; right: 20px !important; z-index: 999999 !important; display: flex !important; flex-wrap: wrap !important; align-items: center !important; padding: 10px !important; background-color: #8F93FF !important; border-radius: 0 0 12px 12px !important; box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.2) !important; font-family: 'Roboto', sans-serif !important; } /* Стили кнопок */ .get-id-widget-panel button { background-color: #fff !important; color: #222 !important; border: none !important; border-radius: 8px !important; padding: 10px 20px !important; margin: 0 5px !important; font-size: 14px !important; cursor: pointer !important; transition: background-color 0.2s ease !important; width: 200px !important; } /* Кнопка настроек */ .get-id-widget-panel button:nth-child(4) { background-color: #6366cf !important; color: #fff !important; width: auto !important; padding: 10px !important; } /* Эффект наведения на кнопки */ .get-id-widget-panel button:hover { opacity: .85 !important; } /* Тултип для отображения буфера */ .get-id-widget-tooltip { position: fixed !important; display: none !important; background-color: #fff !important; color: #222 !important; border: 1px solid #8F93FF !important; padding: 12px 16px !important; border-radius: 8px !important; font-size: 14px !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; white-space: pre-line !important; max-width: 500px !important; z-index: 999999 !important; top: 70px !important; right: 20px !important; } /* Видимость тултипа */ .get-id-widget-tooltip.show { display: block !important; } /* Содержимое тултипа */ .tooltip-content { max-height: 300px !important; overflow-y: auto !important; } /* Кнопка очистки буфера */ .get-id-widget-panel button:nth-child(3) { position: absolute !important; bottom: -25px !important; padding: 1px 10px !important; font-size: 12px !important; color: red !important; width: max-content !important; background: none !important; } /* Стили для плашек с ID */ .id-label { position: absolute !important; right: 20px !important; bottom: 20px !important; display: inline-block !important; margin-left: 10px !important; padding: 4px 10px !important; border-radius: 4px !important; color: #000; font-size: 14px !important; cursor: pointer !important; transition: opacity 0.2s ease-out !important; z-index: 100 !important; width: max-content !important; transform: translateZ(0) !important; } /* Эффект наведения на плашки */ .id-label:hover { opacity: 0.8 !important; } /* Убираем прозрачность у training-row, чтобы ID были видны */ .training-row td a { opacity: 1 !important; } /* Стили для ID внутри скрытых уроков */ .lesson-is-hidden .id-label { margin-left: 5px !important; vertical-align: middle !important; } /* Стили для панели настроек */ .get-id-widget-settings { position: fixed !important; top: 80px !important; right: 20px !important; background-color: white !important; padding: 15px !important; border-radius: 8px !important; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2) !important; z-index: 9999999 !important; width: 300px !important; } .settings-header { font-size: 16px !important; font-weight: bold !important; margin-bottom: 15px !important; color: #333 !important; } .settings-row { margin-bottom: 10px !important; display: flex !important; flex-direction: column !important; } .settings-row label { margin-bottom: 5px !important; font-size: 14px !important; color: #444 !important; } .settings-row input { padding: 8px !important; border: 1px solid #ddd !important; border-radius: 4px !important; font-size: 14px !important; } .settings-info { font-size: 12px !important; color: #666 !important; margin-bottom: 15px !important; } .settings-buttons { display: flex !important; justify-content: space-between !important; } .settings-buttons button { padding: 8px 15px !important; border: none !important; border-radius: 4px !important; font-size: 14px !important; cursor: pointer !important; } #save-settings { background-color: #8F93FF !important; color: white !important; } #cancel-settings { background-color: #f5f5f5 !important; color: #333 !important; } .settings-buttons button:hover { opacity: 0.9 !important; } `; document.head.appendChild(styleElement); } // ====== ИНИЦИАЛИЗАЦИЯ ПРИ ЗАГРУЗКЕ ====== window.addEventListener('load', () => { if (!document.body) { console.error('Body element not found'); return; } addWidget(); showIds(); }); // Подключаем observer observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); })();