P_Woland / Highlighting of any author comments on YouTube.com

// ==UserScript==
// @name           Highlighting of any author 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.2 (beta)
// @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)
//Edit_End

    const searchAttribute = "youtube-comment-search-plugin::searched"
    const searchAttributeValue= "highlighted"
    const textInInputForm = 'Введи ник для выделения комментария...'
    const buttonText = 'HighLight'
    const txtFieldDisabledMsg = 'Ну, что нашёл, то выделил...'
    const msgInputNickNameDisabled = `Ну, что ты тыкаешь на него,
             почём зря?!`
    const msgInputNickName = `   Сначала ник автора введи,
            а потом уже тыкай
своими шалавливыми ручонками,
                      умник!`

    //Объявление глобальных переменных. Знаю, плохой тон.
    //Пережиток найденного мной кода. Не хотел заморачиваться и лопатить весь код.
    //В любом случае весь этот скрипт обёрнут в общую функцию, запускаемую обезьянкой,
    //и все переменные не выйдут за её пределы.
    let canvas, ctx, xc, yc, options, i, button


// --- Несколько мини функций ---
    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) {
        if (element.value == textInInputForm) {
            element.value = ''
            element.style.fontWeight = 'bold'
        }
    }
    function chekInputFieldIsNotEmpty(inputValue) {
        if (inputValue == textInInputForm || inputValue == '') {
            alert(msgInputNickName)
            return false
        }
        if (inputValue == txtFieldDisabledMsg) {
            alert(msgInputNickNameDisabled)
            return false
        }
        // запускаем поиск и выделение комментов и почти одновременно рисуем прогресс-бар
        setTimeout (function () {findCommentsOfRandomNickName(inputValue)}, 100)
        draw()
    }
// --- Несколько мини функций ---End---



// --- Find & highlights comments ---
    function findCommentsOfRandomNickName(findStr) {
        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)
            }
        }
        setTimeout(findCommentsOfRandomNickName, 500, findStr)
    }
// --- Find & highlights comments ---End---


// --- Create Widget ---
    function createWidget() {
        let btnAndCanvas = document.createElement('div')
        btnAndCanvas.className = 'youtube-myWrap'

        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})

        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.addEventListener('focus', function() {inputOnFocus(this); return false})
        textArea.addEventListener('blur', function() {inputOnBlur(this); return false})
        textArea.addEventListener('keydown', function(e) {if (e.keyCode == 13) {
                                chekInputFieldIsNotEmpty(textArea.value)
                                }; return false
                             })
        let container = document.createElement('div')
        container.className = 'youtube-my-widget-conteiner'

        container.prepend(textArea)
        container.prepend(btnAndCanvas)

        return container
    }
// --- Create Widget ---End---


// --- Create CSS Stylsheet ---
    function createStyleSheet() {
        let sheet = document.createElement('style')
        sheet.type = 'text/css'
        sheet.id = 'youtube-css-sheet-button'

        let sheetContent = `
            .youtube-myCanvas {
                display : block;
                margin : 0 auto 10px;
            }
            .youtube-myWrap {
                padding-bottom : 10px;
                margin-right: 10px;
                display : inline-block;
                //background-color : rgba(240,240,240,0.5);
                border-style : groove;
                border-radius : 20px;
            }
            .youtube-myButton {
                width : 70%;
                height : 25px;
                display : block;
                font-weight : 500;
                font-size : 12px;
                //line-height : 30px;
                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-my-input-field {
                color : #ccc
                background-color : #eee;
                display : inline-block;
                width : 400px;
                margin : 20px;
                margin-left : 0px;
                border-style : groove;
                border-radius : 10px;
                padding : 3px;
                border-color : #555;
            }
            .youtube-my-widget-conteiner {
                background-color : rgba(153,153,153,0.5);
                display : inline;
                padding : 15px;
                border-style: groove;
                border-radius : 20px;
            }
            .disable, .disable : hover {
                border : solid 1px #357ebd;
                background-color : #428bca;
                opacity : 0.4;
                cursor : default;
            }
        `
        sheet.innerHTML = sheetContent
        document.body.prepend(sheet)
    }
// --- Create CSS Stylsheet ---End---


// --- Progress Bar Functions ---
    function getRadians(degree) {
        // переводим градусы в радианы
        return Math.PI / 180 * degree
    }

    function init() {
        // длительность отрисовки одного сектора
        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 draw() {
        // получаем из массива пару цветов, которая будет использоваться
        // для создания градиента 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')

                    // удаляем зарегистрированный обработчик события
                    //Не удаляем, так как я переписал логику добавления обработчика к кнопке//
                    //button.removeEventListener('click', draw)

                    // выходим из функции рисования прогресс бара
                    return
                }
                // угол, с которого начинает отрисовываться следующий сектор
                options.start += options.step
                // запускаем рисование следующего сектора, рекурсивно
                // вызывая функцию draw
                return draw()
                }
            }
        // старт анимации отрисовки одного сектора
        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---


    function findElement() {
        let parentDiv = document.querySelector('#primary-inner') //Ищем контейнер верхнего уровня с наверняка уникальным id
        let childDiv = parentDiv.querySelector('#contents') //и только внутри него ищем "ребёнка" уже янвно с неуникальным id.
        if (!childDiv) {
            setTimeout(findElement, 250)
            return false
            } else {//Если страница не догрузилась, перезапускаем через четверь секунды.
                createStyleSheet() //Именно это она и делает :)))
                let firstParent = childDiv.parentNode //Добираемся по DOM-у до нужного родительского узла.
                let element = createWidget()
                firstParent.prepend(element) //Вставляем виджет перед всеми его потомками.


                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
                //запускаем настройку отрисовки прогресс бара
                init()
            }
    }
    setTimeout(findElement, 3000) //Ну, не сразу скрипт запускаем, не надо сразу.

})();