MjKey / Trovo Цветные Ники

// ==UserScript==
// @name         Trovo Цветные Ники
// @name:ru      Trovo: Цветные Ники
// @name:en      Trovo: Color Nick (Names)
// @namespace    http:/mjkey.ru/
// @version      0.0.2 * Больше цветов!
// @description  Цветные ники для Trovo | EN Author (Owner): yyko
// @description:ru  Цветные ники для Trovo | EN Author (Owner): yyko
// @description:en  Color nicknames for Trovo | EN Author (Owner): yyko
// @author       MjKey | Owner: yyko
// @match        https://trovo.live/*
// @icon         https://icons.duckduckgo.com/ip2/trovo.live.ico
// @license      MIT
// @run-at       document-end
// @updateURL https://openuserjs.org/meta/MjKey/Trovo_Цветные_Ники.meta.js
// @downloadURL https://openuserjs.org/install/MjKey/Trovo_Цветные_Ники.user.js
// @grant        GM_addStyle
// @copyright    yyko
// @collaborator MjKey
// ==/UserScript==

(function () {
  'use strict';

  const maxAttemptsCount = 10;
  const attmeptDelay = 2000;

  const colorMap = new Map([
    ["red", "#FF0000"], //красный
    ["blue", "#0000FF"], //синий
    ["green", "#008000"], //зелёный
    ["firebrick", "#B22222"], //кирпичный
    ["coral", "#FF7F50"], //коралловый
    ["yellowgreen", "#9ACD32"], //лайм
    ["orangered", "#FF4500"], //красно-оранжевый
    ["seagreen", "#2E8B57"], //морская волна
    ["goldenrod", "#DAA520"], //красное золото
    ["chocolate", "#D2691E"], //шоколадный
    ["cadetblue", "#5F9EA0"], //серо-голубой
    ["dodgerblue", "#1E90FF"], //васильковый
    ["hotpink", "#FF69B4"], //ярко-розовый
    ["blueviolet", "#8A2BE2"], //индиго
    ["aqua", "#00FFFF"], //аква
  ]);
  const colorNames = Array.from(colorMap.keys());

  // Значёк палитры от Google (https://www.flaticon.com/authors/google)
  const colorizerSvg = '<svg aria-hidden="true" class="svg-icon btn-icon size24" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="24" height="24"><path d="M12 1.5C6.202 1.5 1.5 6.202 1.5 12S6.202 22.5 12 22.5a1.748 1.748 0 0 0 1.295-2.923 1.733 1.733 0 0 1-.437-1.16c0-.969.781-1.75 1.75-1.75h2.059a5.835 5.835 0 0 0 5.833-5.834C22.5 5.677 17.798 1.5 12 1.5zM5.583 12c-.968 0-1.75-.782-1.75-1.75s.782-1.75 1.75-1.75c.969 0 1.75.782 1.75 1.75S6.552 12 5.583 12zm3.5-4.667c-.968 0-1.75-.781-1.75-1.75 0-.968.782-1.75 1.75-1.75.969 0 1.75.782 1.75 1.75 0 .969-.781 1.75-1.75 1.75zm5.834 0c-.969 0-1.75-.781-1.75-1.75 0-.968.781-1.75 1.75-1.75.968 0 1.75.782 1.75 1.75 0 .969-.782 1.75-1.75 1.75zm3.5 4.667c-.969 0-1.75-.782-1.75-1.75s.781-1.75 1.75-1.75c.968 0 1.75.782 1.75 1.75S19.385 12 18.417 12z" style="stroke-width:.0546871"/></svg>';

  const obsConfig = {
    childList: true
  };
  const classPrefix = 'clrz_';

  const cpCSSbase = '.cp_list,.cp_color{display: inline-block;line-height: 0px;}.cp_list{max-width: 125px;}.cp_color{margin: 0px;width: 25px;height: 25px;transition: all 0.2s ease;}.cp_color_active{box-shadow: 0 0 0 5px white inset;}';

  const onlyChat = !!document.location.href.match('https://trovo.live/chat/.*');

  // локальное хранилище

  function mapToStr(map) {
    return JSON.stringify(Object.fromEntries(map));
  }

  function strToMap(str) {
    return new Map(Object.entries(JSON.parse(str)));
  }

  function loadData(entryName = 'users') {
    let data = localStorage.getItem(entryName);
    if (data) {
      return strToMap(data);
    }
    else {
      return null;
    }
  }

  function saveData(data, entryName = 'users') {
    localStorage.setItem(entryName, mapToStr(data));
  }

  // --

  // инструменты пользователя

  let sessionUsers = new Map();
  let users = loadData();
  if (users) {
    if (localStorage.getItem('tncts_localUsers')) {
      localStorage.removeItem('tncts_localUsers');
    }
  }
  else {
    users = new Map();
  }

  function addUser(name) {
    let userColor = getRandomColorName();
    createUserClass(name, userColor);
    changeUserColor(name, userColor);
    return userColor;
  }

  function delUser(name) {
    users.delete(name);
    saveData(users);
  }

  // --

  // инструменты цвета

  function getRandomColorName() {
    return colorNames[Math.round(Math.random() * colorNames.length)];
  }

  function getUserColor(name) {
    return users.get(name);
  }

  function changeUserColor(name, colorName) {
    let ccr = checkColor(colorName);
    if (ccr) {
      let cv;
      if (ccr == 1) {
        cv = colorMap.get(colorName.toLocaleLowerCase());
      }
      else if (ccr == 2) {
        cv = colorName;
      }
      changeUserClass(name, cv);
      users.set(name, cv);
      saveData(users);
    }
  }

  function checkColor(colorName) {
    if (colorMap.has(colorName)) {
      return 1;
    }
    else if (colorName.match(/^#[0-9a-f]{3,4}$|^#(?:[0-9a-f]{2}){3,4}$/)) {
      return 2;
    }
    else {
      return false;
    }
  }

  function findColorByCode(cc) {
    for (let i of colorMap) {
      if (i[1] == cc)
        return i[0];
    }
  }

  // --

  // стилизация

  let ss;

  function initSS() {
    if (ss)
      document.head.appendChild(ss);
    else
      ss = GM_addStyle(makeStyleText());
  }

  function createUserClass(name, color) {
    ss.innerHTML = ss.innerHTML + `.${getUserClassName(name)}{color:${color} !important;}`;
    sessionUsers.set(name, color);
  }

  function changeUserClass(name, newcolor) {
    let ucn = getUserClassName(name);
    ss.innerHTML = ss.innerHTML.replace(`.${ucn}{color:${getUserColor(name)} !important;}`, `.${ucn}{color:${newcolor} !important;}`);
  }

  function getUserClassName(name) {
    return `${classPrefix}${name}`;
  }

  // --

  // палитра

  function makeStyleText(lumshift = 0.2) {
    let cpCSS = cpCSSbase;

    function cpbCSS(cn, cc) {
      function parseCol(a) {
        let outp = [];
        a = a.slice(1);
        for (let i = 0; i < 3; i++) {
          outp.push(parseInt(a.slice(i * a.length / 3, (i + 1) * a.length / 3), 16));
          if (a.length == 3)
            outp[i] *= 16;
        }
        return outp;
      }

      function clamp(a, min, max) {
        return (a < min) ? min : (a > max) ? max : a;
      }

      function hex(from) {
        let outp = '#',
          tl = true,
          t;

        for (let i in from)
          from[i] = Math.round(from[i]);

        for (let i of from)
          tl = tl & (i % 0x11 == 0);

        for (let i of from) {
          t = i.toString(16);
          outp += (tl) ? t[0] : (t.length > 1) ? t : '0' + t;
        }
        return outp;
      }

      function slum(col, s) {
        col = col.slice();
        let as = s * 3 * 0xff;
        let sow = 0;
        if (s > 0) {
          for (let i of col)
            sow += 1 - i / 0xff;

          for (let i = 0; i < 3; i++)
            col[i] = clamp(col[i] + (1 - col[i] / 0xff) / sow * as, 0, 0xff);
          return col;
        }
        else {
          for (let i of col)
            sow += i / 0xff;

          for (let i = 0; i < 3; i++)
            col[i] = clamp(col[i] + col[i] / 0xff / sow * as, 0, 0xff);
          return col;
        }
      }

      function shiftCol(col, s) {
        return hex(slum(parseCol(col), s));
      }
      return `.cp_color_${cn}{background-color: ${cc};}.cp_color_${cn}:hover{background-color: ${shiftCol(cc,lumshift)};}`;
    }

    for (let i of colorMap)
      cpCSS += cpbCSS(i[0], i[1]);

    return cpCSS;
  }

  function createPalleteElement(lstn) {
    function n(t) {
      return document.createElement(t);
    }
    let outp = n('div');
    outp.className = 'cp_list';

    function onselect(e, me, silent = false) {
      me = me || this;
      for (let i of outp.childNodes)
        i.classList.remove('cp_color_active');

      me.classList.toggle('cp_color_active');
      if (!silent && lstn)
        lstn(outp.nick, me.getAttribute('mycolor'));
    }

    let colblock;
    outp.colblocks = [];
    for (let i of colorMap) {
      colblock = n('div');
      colblock.className = 'cp_color cp_color_' + i[0];
      colblock.setAttribute('mycolor', i[0]);
      colblock.addEventListener('click', onselect);
      outp.appendChild(colblock);
      outp.colblocks.push(colblock);
    }

    outp.style.position = 'fixed';
    outp.style.zIndex = '99999';
    outp.style.display = 'none';
    outp.setPos = function (x, y) {
      outp.style.left = x + 'px';
      outp.style.top = y + 'px';
    };
    outp.select = function (color) {
      for (let i of outp.colblocks) {
        if (i.getAttribute('mycolor') == color) {
          onselect(null, i, true);
          break;
        }
      }
    };

    return outp;
  }

  function onclick(e) {
    switch (true) {
      case e.target.classList.contains('nick-name'):
        palleteElement.nick = e.target.getAttribute('title');
        palleteElement.select(findColorByCode(getUserColor(palleteElement.nick)));
        //kostyl
        setTimeout(function () {
          try {
            let boxpos = document.getElementsByClassName('card-container')[0].getBoundingClientRect();
            palleteElement.setPos((onlyChat) ? boxpos.x + boxpos.width : boxpos.x - palleteElement.getBoundingClientRect().width, boxpos.y);
          }
          catch (e) {
            console.log('увы, костыль не сработал', e);
          }
        }, 123);
        //--kostyl
      case e.target.classList.contains('cp_color'):
        palleteElement.style.display = 'block';
        break;
      default:
        palleteElement.style.display = 'none';
        palleteElement.nick = null;
        break;
    }
    //return false;
  }

  // --

  function onmessage(mutations, observer) {
    for (let mutation of mutations) {
      for (let msgel of mutation.addedNodes) {
        let nameel = msgel.getElementsByClassName('nickname-box')[0];
        let name;
        if (nameel) {
          name = nameel.getElementsByClassName('nick-name')[0].title;
          if (!users.has(name))
            addUser(name);
          else if (!sessionUsers.has(name))
            createUserClass(name, getUserColor(name));

          // обработка команды изменения цвета
          let msgtextel = msgel.getElementsByClassName('content')[0];
          if (msgtextel) {
            let msgtext = msgtextel.innerText;
            let res = msgtext.match(/^!color (.*)/);
            if (res) {
              let colorValue = res[1];
              if (checkColor(colorValue)) {
                changeUserColor(name, res[1]);
              }
              else {
                console.warn('цвет недоступен');
              }
            }
          }

          // применение цвета
          nameel.classList.add(getUserClassName(name));
        }
      }
    }
  }

  // инициализация

  let launched;
  let launching;
  let chatElement;
  let chatObserver;
  let palleteElement;

  function findChatElement() {
    return document.getElementsByClassName('chat-list')[0];
  }

  let attemptsLeft = maxAttemptsCount;
  let attemptsTimer;

  function initChat() {
    launched = false;
    launching = true;

    chatElement = findChatElement();
    if (chatElement) {
      initSS();

      palleteElement = createPalleteElement(changeUserColor);
      document.body.appendChild(palleteElement);
      window.addEventListener('mouseup', onclick);

      chatObserver = new MutationObserver(onmessage);
      chatObserver.observe(chatElement, obsConfig);

      launched = true;
      launching = false;
      console.warn('стартуем');
    }
    else {
      if (attemptsLeft > 0) {
        console.warn('Попыток старта: ', attemptsLeft--);
        attemptsTimer = setTimeout(initChat, attmeptDelay);
      }
      else {
        console.warn('не найден чат');
        launching = false;
      }
    }
  }

  function initBaseObs() {
    let baseElement = document.getElementsByClassName('base-container')[0];
    if (baseElement) {
      let baseObserver = new MutationObserver(onbasechanged);
      baseObserver.observe(baseElement, obsConfig);
    }
    else {
      console.warn('не найден базовый контейнер');
    }
  }

  function onbasechanged() {
    if (findChatElement()) {
      if (!launching && !launched)
        initChat();
    }
    else {
      if (launched) {
        console.warn('остановка');
        chatObserver.disconnect();
        ss.remove();
        attemptsLeft = maxAttemptsCount;
        launched = launching = false;
      }
    }
  }

  function init() {
    initBaseObs();
    initChat();
  }

  init();

  // --
})();