P_Woland / Highlighting comments on YouTube.com

// ==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---

})();