NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Highlighting comments on YouTube.com // @name:ru Подсветка комментариев на YouTube.com // @description Search and highlight comments of any author on YouTube.com. Doesn't break links. Works with Ajax. // @description:ru Поиск и подсветка комментариев любого автора на YouTube.com. Не ломает ссылки. Работает с Ajax. // @namespace http://tampermonkey.net/ // @author Professor Woland (P_Woland) // @developer Professor Woland (P_Woland) // @license MIT // @version 1.3.3 // @match http*://www.youtube.com/* // @grant none // ==/UserScript== (function() { 'use strict'; //Edit const color = 'GreenYellow' // Highlight color const percent = '200%' //Nickname size as a percentage const fontWeight = 900 //The most bold font possible (100-900) const delay_inputArea = 100 // Период вывода символов при автопечати (ms) const lettersNum_inputArea = 1 //Количество выводимых символов за итерацию цикла const textInInputForm = 'Введи ник для выделения комментария...' //Выводимый текст const txtFieldDisabledMsg = 'Количество найденных комментариев ➜' //Текст после поиска const buttonText = 'HighLight' //Текст на кнопке. //Параметры алерта при попытке пустого поиска. const ttl1 = 'Attention!' //Заглавие const msg1 = 'Сначала ник автора введи, а потом уже тыкай своими шалавливыми ручонками, умник!'//Тело сообщения const icn1 = 'warning'//Иконка ("warning", "error", "success" or "info") const btnTxt1 = 'Понял, учту...'//Текст на кнопке алерта const outClose1 = false//Закрывать по клику вне алерта (true, false) const escClose1 = true//Закрывать алерт эскейпом (true, false) //Параметры алерта при попытке повторного поиска. const ttl2 = 'Achtung!!!' const msg2 = 'Ты мне не тычь!!! Поиск уже завершён!' const icn2 = 'error' const btnTxt2 = 'Зачем ругаися, нащяльника?' const outClose2 = false const escClose2 = true //Edit_End // --- Объявление глобальных переменных --- //Название атрибута и его значение, присваиваемые узлу после удачного подсвечивания. const searchAttribute = "youtube-comment-search-plugin::searched" const searchAttributeValue= "highlighted" //Дескриптор автопечати для прерывания. let letterTypingTimerId //Готовые параметры для "сладких алертов". //Опционально. Так лучше видно, что будет в алерте, чем в коде писать: //swal({title: ttl1, text: msg1, icon: icn1, button: btnTxt1, closeOnClickOutside: outClose1, closeOnEsc: escClose1,}) const msgOnEmptyInput = { title: ttl1, text: msg1, icon: icn1, button: btnTxt1, closeOnClickOutside: outClose1, closeOnEsc: escClose1, //timer: 2000,//Автоскрытие алерта через (ms) } const msgOnAfterFindAndHighlight = { title: ttl2, text: msg2, icon: icn2, button: btnTxt2, closeOnClickOutside: outClose2, closeOnEsc: escClose2, } /*Объявление глобальных переменных для прогресс-бара. Знаю, плохой тон. Пережиток найденного мной кода. Не хотел заморачиваться и лопатить весь код, переписывая его. В любом случае весь этот скрипт обёрнут в общую функцию, запускаемую обезьянкой, и все переменные не выйдут за её пределы.*/ let canvas, ctx, xc, yc, options, i, button // --- Объявление глобальных переменных ---End--- // --- Функция-загрузчик внешних JS и CSS файлов --- //Можно также внедрять текст таблицы стилей //fileType = ['js' ,'css' or 'sheet']. //В последнем случае передаётся не URI, а текст таблицы. function myJsCssLoader(fileType, fileUriOrStyleContent) { let tagType switch (fileType) { case 'js': { tagType = document.createElement('script') tagType.setAttribute('type', 'text/javascript') tagType.setAttribute('src', fileUriOrStyleContent) break } case 'css': { tagType = document.createElement('link') tagType.setAttribute('rel', 'stylesheet') tagType.setAttribute('type', 'text/css') tagType.setAttribute('href', fileUriOrStyleContent) break } case 'sheet': { tagType = document.createElement('style') tagType.setAttribute('type', 'text/css') tagType.innerHTML = fileUriOrStyleContent break } default: { alert('Что-то пошло не так') break } } if (typeof(tagType) != 'undefined') { document.getElementsByTagName('head')[0].append(tagType) } } // --- Функция-загрузчик внешних JS и CSS файлов ---End--- // --- Несколько мини функций --- function findPlaceToInsertElement() { let parentDiv = document.querySelector('#primary-inner') //Ищем контейнер верхнего уровня с наверняка уникальным id let childDiv = parentDiv.querySelector('#contents') //и только внутри него ищем "ребёнка" уже янвно с неуникальным id. return childDiv } function highlightComment(element) { element.style.fontWeight = fontWeight element.style.backgroundColor = color } function highlightNickname(element) { element.style.backgroundColor = color element.style.fontSize = percent } function inputOnBlur(element) { if (element.value == '') { element.value = textInInputForm element.style.fontWeight = 'normal' } } function inputOnFocus(element) { element.style.fontWeight = 'bold' element.style.color = 'black' if (element.value == textInInputForm) { element.value = '' } } function counterIncrement(cntDiv) { cntDiv.innerHTML = parseInt(cntDiv.innerHTML)+1 } // --- Несколько мини функций ---End--- // --- Проверка поля ввода на пустоту или дефолтный текст --- function chekInputFieldIsNotEmpty(inputValue) { if (inputValue == textInInputForm || inputValue == '') { swal(msgOnEmptyInput) //"swal is not defined" ибо функция находится во внешнем .js файле. return false } if (inputValue == txtFieldDisabledMsg) { swal(msgOnAfterFindAndHighlight) return false } // запускаем поиск и выделение комментов и одновременно рисуем прогресс-бар setTimeout (function () {findCommentsOfRandomNickName(inputValue)}, 100) drawProgressBar() } // --- Проверка поля ввода на пустоту или дефолтный текст ---END--- // --- Автопечать текста в поле input --- function typeText(source, dest, lettersNum, delay, eventListenerOwner) { //Параметры в по порядку: //1.откуда берём текст, //2.куда вставляем, //3.по сколько символов печатаем за итерацию, //4.период вывода символов, //5.где сработал обработчик dest.value = "" let sourceLen = source.length setTimeout (typeLetters, delay, source, sourceLen, dest, 0, lettersNum, delay) dest.style.color = 'red' dest.style.fontWeight = 'bold' //Убираем обработчик с объекта запуска eventListenerOwner.onmouseover = function() {return false} } function typeLetters(source, sourceLen, dest, startIndex, lettersNum, delay) { //source - откуда берём текст, //sourceLen - кол-во символов в источнике //dest - куда вставляем, //startIndex - порядковый номер символа для печати //lettersNum - по сколько символов печатаем за итерацию, //delay - период вывода символов dest.value += source.substr(startIndex, lettersNum) startIndex += lettersNum if (startIndex < sourceLen) { letterTypingTimerId = setTimeout(typeLetters, delay, source, sourceLen, dest, startIndex, lettersNum, delay) } else {dest.style.color = 'black' dest.style.fontWeight = 'normal' } } // --- Автопечать текста в поле input ---End--- // --- Find & highlights comments --- function findCommentsOfRandomNickName(findStr) { //Устанавливаем счётчик в нуль let cntDiv = document.getElementById('youtube-counter-of-highlight-messages') if (cntDiv.innerHTML == '') { cntDiv.innerHTML = 0 cntDiv.classList.add('youtube-myCounter-visible') } //Ищем контейнеры с комментариями let comments = document.querySelectorAll('#main') for (const cmnt of comments) { //Если коммент был ранее помечен, пропускаем и ищем далее if (cmnt.getAttribute(searchAttribute) == searchAttributeValue) {continue} //Если не помечен, то проверяем встречается ли в имени автора искомый let author = cmnt.querySelector("#author-text > span") if (author && author.innerHTML.includes(findStr)) { //Если встречается - беспощадно метим и ник, и коммент. let content = cmnt.querySelector("#content-text") highlightComment(content) highlightNickname(author) cmnt.setAttribute(searchAttribute, searchAttributeValue) //и инкрементируем счётчик выделенных комментов. counterIncrement(cntDiv) } } //Перезапуск поиска автора коммента по кругу каждые полсекунды setTimeout(findCommentsOfRandomNickName, 500, findStr) } // --- Find & highlights comments ---End--- // --- Create Widget --- function createWidget() { //прогресс-бар let cnvs = document.createElement('canvas') cnvs.id = 'youtube-myCanvas' cnvs.width = '100' cnvs.height = '100' //кнопка let btn = document.createElement('button') btn.type = 'button' btn.className = 'youtube-myButton' btn.innerText = buttonText btn.id = 'youtube-progress-bar-button' btn.addEventListener('click', function() {chekInputFieldIsNotEmpty(textArea.value); return false}) //Контейнер для кнопки и прогресс-бара let btnAndCanvas = document.createElement('div') btnAndCanvas.className = 'youtube-myWrap' //Внедряем кнопку с баром в контейнер btnAndCanvas.append(cnvs) btnAndCanvas.append(btn) //Поле ввода ника автора коммента let textArea = document.createElement('input') textArea.id = 'youtube-find-nickname-text' textArea.value = textInInputForm textArea.className = 'youtube-my-input-field' textArea.onmouseover = function() {typeText (textInInputForm, this, lettersNum_inputArea, delay_inputArea, this) return false} textArea.addEventListener('focus', function() {clearTimeout(letterTypingTimerId) inputOnFocus(this) return false}) textArea.addEventListener('blur', function() {inputOnBlur(this) return false}) textArea.addEventListener('keydown', function(e) {if (e.keyCode == 13) {chekInputFieldIsNotEmpty(this.value)} return false}) //Счётчик количества найденных комментов let couterDiv = document.createElement('div') couterDiv.id = 'youtube-counter-of-highlight-messages' couterDiv.className = 'youtube-myCounter' //Родительский контейнер виджета let container = document.createElement('div') container.className = 'youtube-my-widget-conteiner' container.prepend(couterDiv) container.prepend(textArea) container.prepend(btnAndCanvas) return container } // --- Create Widget ---End--- // --- Create CSS Stylsheet --- function createStyleSheet() { let sheetContent = ` .youtube-myCanvas { display: block; margin: 0 auto 10px; } .youtube-myButton { width: 70%; height: 25px; display: block; font-weight: 500; font-size: 12px; font-family: Roboto, sans-serif; color: #eee; text-align: center; margin: 0 auto; border: solid 1px #333; -webkit-border-radius: 3px; border-radius: 10px; outline: none; -webkit-user-select: none; user-select: none; background-color: #888; cursor: pointer; transition: all 0.3s; } .youtube-myButton:hover { border-color: #285e8e; background-color: #3276b1; } .youtube-myWrap { display: inline-block; border-style: groove; border-radius: 20px; } .youtube-my-input-field { display: inline-block; width: 400px; margin: 20px; margin-left: 0px; margin-right: 5px; border-style: groove; border-radius: 10px; padding: 3px; border-color: #555; font-size: 10pt; } .youtube-my-widget-conteiner { background-color: rgba(153,153,153,0.5); display: inline; padding: 15px; padding-left: 0px; padding-bottom: 12px; border-style: groove; border-radius: 20px; } .youtube-myCounter { display: inline; font-size: 12pt; color: white; font-weight: 600; text-align: center; padding: 5px; border-style: groove; border-radius: 20px; border-color: #555; } .youtube-myCounter-visible { background-color: #8c8c8c; border-width: 2px; } .youtube-myCounter::selection, .youtube-myCounter::-moz-selection, .youtube-my-input-field-disable::selection, .youtube-my-input-field-disable::-moz-selection { background: transparent; } .disable, .disable:hover { border: solid 1px #357ebd; background-color: #428bca; opacity: 0.4; cursor: default; } ` return sheetContent } // --- Create CSS Stylsheet ---End--- // --- Progress Bar Functions --- function getRadians(degree) { // переводим градусы в радианы return Math.PI / 180 * degree } function initProgressBar() { //Создаём область отрисовки прогресс-бара canvas = document.getElementById('youtube-myCanvas') //контекст, через который будем управлять содержимым canvas ctx = canvas.getContext('2d') //центр по горизонтали и вертикали xc = canvas.width / 2 yc = canvas.height / 2 //объект содержащий настройки options = {} //объект кнопки, запускающей прогресс бар button = document.querySelector('#youtube-progress-bar-button') i = 0 // длительность отрисовки одного сектора options.duration = 200 // массив со значениями цвета начала и конца градиента секторов options.colors = ['#f00', '#ff2f00', '#ff7e00', '#ffde00', '#dffc00', '#7ae000', '#2cbb00', '#15b200'] // шаг отрисовки цветов (размер сектора) в радианах options.step = getRadians(45) // получаем угол начала прогресс бара в радианах options.start = getRadians(112.5) // ширина прогресс бара в px options.width = 30 // радиус прогресс бара в px options.r = xc - options.width // очищаем canvas ctx.clearRect(0, 0, canvas.width, canvas.height) // рисуем подложку без анимации drawSector('#eee', options.width) } function drawProgressBar() { // получаем из массива пару цветов, которая будет использоваться // для создания градиента i-го сектора прогресс бара let startColor = options.colors[i], endColor = options.colors[i + 1] // получаем координаты X, Y точек начала и конца i-го сектора прогресс бара let x0 = xc + Math.cos(options.start) * options.r, y0 = yc + Math.sin(options.start) * options.r, x1 = xc + Math.cos(options.start + options.step) * options.r, y1 = yc + Math.sin(options.start + options.step) * options.r // используя метод createLinearGradient, создаём объект линейного градиента, // в качестве аргументов метод принимает значения координат начала и конца // сектора, к которому он будет применён let gradient = ctx.createLinearGradient(x0, y0, x1, y1) // используя метод addColorStop определяем цвет // в начале объекта градиента gradient.addColorStop(0, startColor) // в конце объекта градиента gradient.addColorStop(1.0, endColor) // старт анимации отрисовки одного сектора let start = new Date().getTime() let fn = function() { // время прошедшее от начала отрисовки сектора let now = new Date().getTime() - start // если текущее время превысило время анимации, присваиваем ему значение // времени анимации, в противном случае, сектор может получиться // большего размера, чем планировалось now = (now < options.duration) ? now : options.duration // на сколько должен быть отрисован текущий сектор let inc = options.step * now / options.duration // предварительно закрашиваем текущий сектор белым цветом на угол равный inc // толщину берём на 2px больше, чтобы закрасить возможные артефакты drawSector('#fff', options.width + 2, inc) // закрашиваем текущий сектор градиентом на угол равный inc drawSector(gradient, options.width, inc) // закрашиваем стыки секторов drawLine(i) // выводим проценты заполнения прогресс бара showPercents(i, inc) // если текущее время меньше времени анимации, // продолжаем рисование текущего сектора if (now < options.duration) { requestAnimationFrame(fn) } else { // увеличиваем индекс на единицу, чтобы выбрать из массива цветов следующую пару i++ // все сектора отрисованы, заканчиваем работу функции if (i >= options.colors.length - 1) { // делаем кнопку запуска прогресс бара неактивно //Так же делаем неактивным поле ввода// button.classList.add('disable') //Можно раскомментировать и полностью деактивировать кнопку, но тогда //не будет появляться алерт с матюгами. //button.setAttribute('disabled', 'disabled') let txtField = document.getElementById('youtube-find-nickname-text') txtField.value = txtFieldDisabledMsg txtField.setAttribute('readonly', 'readonly') txtField.classList.add('youtube-my-input-field-disable') // удаляем зарегистрированный обработчик события //Не удаляем, так как я переписал логику добавления обработчика к кнопке// //button.removeEventListener('click', drawProgressBar) // выходим из функции рисования прогресс бара return } // угол, с которого начинает отрисовываться следующий сектор options.start += options.step // запускаем рисование следующего сектора, рекурсивно // вызывая функцию drawProgressBar return drawProgressBar() } } // старт анимации отрисовки одного сектора requestAnimationFrame(fn) } function drawSector(colorFill, widthWheel, inc) { // beginPath используется чтобы начать серию действий, описывающих отрисовку фигуры. // каждый новый вызов этого метода сбрасывает все действия предыдущего и начинает // рисовать заново ctx.beginPath() // устанавливаем цвет или стиль, используемый при выполнении обводки ctx.strokeStyle = colorFill // устанавливается ширина линии, которой будет рисоваться дуга ctx.lineWidth = widthWheel // вычисляем конечный угол, если inc не задан, значит рисуется подложка // и задаётся конечный угол прогресс бара let end = (inc === undefined) ? getRadians(427.5) : options.start + inc // создаётся дуга, где xc и yc центр окружности, далее радиус, начальный и конечный угол ctx.arc(xc, yc, options.r, options.start, end) // рисуется дуга (часть сектора), с параметрами заданными с помощью // strokeStyle, lineWidth и arc ctx.stroke() return } function showPercents(i, inc) { // угол в радианах, на который отрисован прогресс бар на текущий момент let angle = options.step * i + inc, // получаем проценты, где 0.0549779 результат деления options.step * 7 на 100 percents = Math.ceil(angle / 0.0549779) // цвет текста ctx.fillStyle = '#666' // параметры шрифта и текста ctx.font = '400 12px Roboto' // центрирование текста по горизонтали ctx.textAlign = 'center' // центрирование текста по вертикали ctx.textBaseline = 'center' if (percents == 100) { ctx.font = '900 12px Roboto' ctx.fillStyle = 'green' } // очищаем область canvas в которую будет выведен текст // область представлена в виде прямоугольника заданного // начальной точкой (120px,125px), шириной и высотой (60px,30px) // отсчёт координат идёт от верхнего левого угла canvas ctx.clearRect(34, 37, 33, 17) // выводим текст в центр canvas ctx.fillText(percents + '%', xc, yc) } function drawLine(i) { // определяем координаты начала и конца линии границы текущего сектора let x0 = xc + Math.cos(options.start) * (options.r + 15), y0 = yc + Math.sin(options.start) * (options.r + 15), x1 = xc + Math.cos(options.start) * (options.r - 15), y1 = yc + Math.sin(options.start) * (options.r - 15) ctx.beginPath() // Вариант 1 - назначаем цвет границы стыка всех секторов //ctx.strokeStyle = '#fff' // Вариант 2 - выбираем цвет стыка текущего и следующего секторов из массива ctx.strokeStyle = options.colors[i] // устанавливаем координаты начала и конца рисуемой линии и её толщину ctx.moveTo(x0, y0) ctx.lineTo(x1, y1) ctx.lineWidth = 1 // рисуем границу секторов ctx.stroke() return } // --- Progress Bar Functions ---End--- // --- Main Function --- function mainFunction() { let nodeToInsertWidget = findPlaceToInsertElement() //Если страница не догрузилась, перезапускаем через секунду и выходим. if (!nodeToInsertWidget) {setTimeout(mainFunction, 1000); return false} //Создаём таблицу стилей и вставляем её в Head myJsCssLoader('sheet', createStyleSheet()) //Грузим внешний JS-файл альтернативных модальных окон. myJsCssLoader('js', 'https://unpkg.com/sweetalert/dist/sweetalert.min.js') //Добираемся по DOM-у до нужного родительского узла и вставляем виджет перед всеми его потомками. nodeToInsertWidget.parentNode.prepend(createWidget()) //Запускаем настройку отрисовки прогресс-бара initProgressBar() } // --- Main Function ---End--- // --- Начало выполнения скрипта --- setTimeout(mainFunction, 2000) //Ну, не сразу скрипт запускаем, не надо сразу. // --- Начало выполнения скрипта ---End--- })();