pid0r / Лог дозора

// ==UserScript==
// @name         Лог дозора
// @namespace    http://tampermonkey.net/
// @version      1.0.0.2
// @description  Дозорим приятнее
// @author       pid0r
// @match        https://catwar.su/cw3/
// @license      MIT; https://opensource.org/licenses/MIT
// ==/UserScript==

(function () {
  'use strict';

  const WIN_WID = 180;
  const WIN_HEI_EXP = 300;
  const WIN_HEI_COL = 'auto';
  const WIN_BG = 'rgb(34, 34, 34)';
  const TXT_COL = 'rgb(131, 131, 131)';
  const BTN_BG = 'rgb(131, 131, 131)';
  const BTN_TXT = 'rgb(34, 34, 34)';

  let isExp = true;
  let isSound = true;
  let catSet = new Set();
  const cont = document.createElement('div');
  cont.style.position = 'fixed';
  cont.style.width = `${WIN_WID}px`;
  cont.style.backgroundColor = WIN_BG;
  cont.style.color = TXT_COL;
  cont.style.zIndex = 10000;
  cont.style.top = '10px';
  cont.style.left = '10px';
  cont.style.borderRadius = '10px';
  cont.style.boxShadow = '0px 2px 4px rgba(0, 0, 0, 0.3)';
  cont.style.overflow = 'hidden';

  const head = document.createElement('div');
  head.style.display = 'flex';
  head.style.justifyContent = 'space-between';
  head.style.alignItems = 'center';
  head.style.padding = '5px';
  head.style.fontWeight = 'bold';
  head.style.cursor = 'move';
  head.innerHTML = 'Лог дозора';

  const togBtn = document.createElement('button');
  togBtn.innerText = '-';
  togBtn.style.backgroundColor = BTN_BG;
  togBtn.style.color = BTN_TXT;
  togBtn.style.border = 'none';
  togBtn.style.cursor = 'pointer';
  togBtn.style.fontWeight = 'bold';
  togBtn.style.fontSize = '20px';
  togBtn.style.width = '30px';
  togBtn.style.height = '30px';
  togBtn.style.borderRadius = '5px';
  head.appendChild(togBtn);

  const contInner = document.createElement('div');
  contInner.style.display = 'block';
  contInner.style.padding = '5px';

  const sndLbl = document.createElement('label');
  sndLbl.style.display = 'flex';
  sndLbl.style.alignItems = 'center';
  sndLbl.style.marginBottom = '5px';

  const sndChk = document.createElement('input');
  sndChk.type = 'checkbox';
  sndChk.checked = isSound;
  sndChk.style.marginRight = '5px';

  sndLbl.appendChild(sndChk);
  sndLbl.appendChild(document.createTextNode('Включить звук'));

  const catList = document.createElement('div');
  catList.style.maxHeight = '200px';
  catList.style.overflowY = 'auto';
  catList.style.marginBottom = '5px';

  const clrBtn = document.createElement('button');
  clrBtn.innerText = 'Очистить лог';
  clrBtn.style.backgroundColor = BTN_BG;
  clrBtn.style.color = BTN_TXT;
  clrBtn.style.border = 'none';
  clrBtn.style.cursor = 'pointer';
  clrBtn.style.width = '100%';
  clrBtn.style.borderRadius = '5px';

  const saveState = () => {
    localStorage.setItem('dozorLogExpanded', isExp);
    localStorage.setItem('dozorLogCats', JSON.stringify([...catSet]));
    localStorage.setItem('dozorLogHeight', cont.style.height);
    localStorage.setItem('dozorLogSound', isSound);
    localStorage.setItem('dozorLogPosition', JSON.stringify({
      top: cont.style.top,
      left: cont.style.left
    }));
  };

  const restoreState = () => {
    const savedPos = JSON.parse(localStorage.getItem('dozorLogPosition'));
    if (savedPos) {
      cont.style.top = savedPos.top;
      cont.style.left = savedPos.left;
    }

    isExp = JSON.parse(localStorage.getItem('dozorLogExpanded'));
    if (!isExp) {
      cont.style.height = `${WIN_HEI_COL}px`;
      contInner.style.display = 'none';
      togBtn.innerText = '+';
    }

    isSound = JSON.parse(localStorage.getItem('dozorLogSound'));
    sndChk.checked = isSound;

    const savedCats = JSON.parse(localStorage.getItem('dozorLogCats'));
    if (savedCats) {
      savedCats.forEach(cat => {
        catSet.add(cat);
        const catElem = document.createElement('div');
        catElem.innerText = cat;
        catList.appendChild(catElem);
      });
    }

    const savedHeight = localStorage.getItem('dozorLogHeight');
    if (savedHeight && isExp) {
      cont.style.height = savedHeight;
    }
  };

  let isDrag = false;
  let startX, startY, initTop, initLeft;

  const onMouseMove = (e) => {
    if (isDrag) {
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;
      cont.style.top = `${initTop + dy}px`;
      cont.style.left = `${initLeft + dx}px`;
    }
  };

  const onMouseUp = () => {
    if (isDrag) {
      isDrag = false;
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
      saveState();
    }
  };

  head.addEventListener('mousedown', (e) => {
    if (e.target === togBtn) return;
    isDrag = true;
    startX = e.clientX;
    startY = e.clientY;
    initTop = cont.offsetTop;
    initLeft = cont.offsetLeft;
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
    e.preventDefault(); // Prevent text selection
  });

  const onTouchMove = (e) => {
    if (isDrag) {
      const touch = e.touches[0];
      const dx = touch.clientX - startX;
      const dy = touch.clientY - startY;
      cont.style.top = `${initTop + dy}px`;
      cont.style.left = `${initLeft + dx}px`;
      e.preventDefault();
    }
  };

  const onTouchEnd = () => {
    if (isDrag) {
      isDrag = false;
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
      saveState();
    }
  };

  head.addEventListener('touchstart', (e) => {
    if (e.target === togBtn) return;
    isDrag = true;
    const touch = e.touches[0];
    startX = touch.clientX;
    startY = touch.clientY;
    initTop = cont.offsetTop;
    initLeft = cont.offsetLeft;
    document.addEventListener('touchmove', onTouchMove, {
      passive: false
    });
    document.addEventListener('touchend', onTouchEnd);
  });

  togBtn.addEventListener('click', () => {
    isExp = !isExp;
    if (isExp) {
      cont.style.height = 'auto';
    }
    else {
      cont.style.height = `${WIN_HEI_COL}px`;
    }
    contInner.style.display = isExp ? 'block' : 'none';
    togBtn.innerText = isExp ? '-' : '+';
    saveState();
  });

  sndChk.addEventListener('change', () => {
    isSound = sndChk.checked;
    saveState();
  });

  clrBtn.addEventListener('click', () => {
    catSet.clear();
    catList.innerHTML = '';
    saveState();
  });

  const addCatToList = (name, id) => {
    const catId = `${name} (${id})`;
    if (!catSet.has(catId)) {
      catSet.add(catId);
      const catElem = document.createElement('div');
      catElem.innerText = catId;
      catList.appendChild(catElem);
      if (isSound) {
        const audio = new Audio('https://abstract-class-shed.github.io/cwshed/action_end.mp3');
        audio.volume = 0.5;
        audio.play();
      }
      cont.style.height = 'auto';
      if (cont.clientHeight > WIN_HEI_EXP) {
        cont.style.height = `${WIN_HEI_EXP}px`;
      }
      saveState();
    }
  };

  const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      if (mutation.type === 'childList' && mutation.addedNodes.length) {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === 1) {
            const catElems = node.querySelectorAll('.cat_tooltip a');
            catElems.forEach(catElem => {
              const name = catElem.innerText;
              const id = catElem.href.match(/cat(\d+)/)[1];
              addCatToList(name, id);
            });
          }
        });
      }
    });
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true
  });

  const chatObserver = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
      if (mutation.type === 'childList' && mutation.addedNodes.length) {
        mutation.addedNodes.forEach(node => {
          if (node.nodeType === 1 && node.matches('.cws_chat_wrapper')) {
            if (isSound) {
              const audio = new Audio('https://abstract-class-shed.github.io/cwshed/action_end.mp3');
              audio.volume = 0.5;
              audio.play();
            }
          }
        });
      }
    });
  });

  const chatContainer = document.querySelector('#cws_chat_msg');
  if (chatContainer) {
    chatObserver.observe(chatContainer, {
      childList: true,
      subtree: true
    });
  }
  else {
    const bodyObserver = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList' && mutation.addedNodes.length) {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType === 1 && node.id === 'cws_chat_msg') {
              chatObserver.observe(node, {
                childList: true,
                subtree: true
              });
            }
          });
        }
      });
    });
    bodyObserver.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  restoreState();

  contInner.appendChild(sndLbl);
  contInner.appendChild(catList);
  contInner.appendChild(clrBtn);
  cont.appendChild(head);
  cont.appendChild(contInner);
  document.body.appendChild(cont);
})();