Telegram Web Sort Panel + Chart + Lang (Зроблено в Україні)

Панель сортировки, график топ-100 сообщений + языковая панель. 🇺🇦 Зроблено в Україні.

// ==UserScript==
// @name         Telegram Web Sort Panel + Chart + Lang (Зроблено в Україні)
// @namespace    https://example.com
// @version      3.3
// @description  Панель сортировки, график топ-100 сообщений + языковая панель. 🇺🇦 Зроблено в Україні.
// @match        https://web.telegram.org/a/*
// @match        https://web.telegram.org/k/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict';

  // === Chart.js ===
  const chartScript = document.createElement('script');
  chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js';
  document.head.appendChild(chartScript);

  GM_addStyle(`
    #ua-panel {
      position: fixed;
      top: 100px; right: 20px;
      width: 300px;
      background: var(--tg-theme-bg-color, #fff);
      color: var(--tg-theme-text-color, #000);
      border-radius: 10px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
      padding: 8px;
      z-index: 99999;
      font-family: Arial,sans-serif;
    }
    #ua-panel-header {
      display:flex;align-items:center;justify-content:space-between;
      font-weight:bold;
      cursor:move;
    }
    #ua-buttons button, #ua-lang select {
      margin:2px;padding:3px 5px;border:none;border-radius:4px;
      background:#0d6efd;color:#fff;cursor:pointer;font-size:12px;
    }
    #ua-buttons button:hover, #ua-lang select:hover {opacity:0.8;}
    #ua-chart {width:100%;height:200px;}
    #ua-hide {
      position:absolute;top:2px;right:2px;cursor:pointer;font-weight:bold;
    }
    #ua-lang {
      margin-top:5px;
    }
  `);

  const panel = document.createElement('div');
  panel.id = 'ua-panel';
  panel.innerHTML = `
    <div id="ua-panel-header">
      <span>🇺🇦 Зроблено в Україні</span>
      <span id="ua-hide">✖</span>
    </div>
    <div id="ua-buttons">
      <button data-sort="reactions">По реакциям</button>
      <button data-sort="date_desc">Дата ↓</button>
      <button data-sort="date_asc">Дата ↑</button>
      <button data-sort="media">По медиа</button>
      <button data-sort="length">По длине</button>
      <button data-action="24h">За 24ч</button>
      <button data-action="refresh">Обновить</button>
      <button data-action="csv">Экспорт CSV</button>
    </div>
    <div id="ua-lang">
      <select>
        <option value="ua">Українська</option>
        <option value="ru">Русский</option>
        <option value="en">English</option>
        <option value="de">Deutsch</option>
        <option value="fr">Français</option>
        <option value="es">Español</option>
        <option value="it">Italiano</option>
        <option value="pt">Português</option>
        <option value="pl">Polski</option>
        <option value="zh">中文</option>
        <option value="ja">日本語</option>
        <option value="ko">한국어</option>
      </select>
    </div>
    <canvas id="ua-chart"></canvas>
  `;
  document.body.appendChild(panel);

  // === drag & drop + сохранение позиции ===
  let offsetX, offsetY, isDragging=false;
  const savedPos = JSON.parse(localStorage.getItem('uaPanelPos')||'{}');
  if(savedPos.left && savedPos.top){
    panel.style.left = savedPos.left+'px';
    panel.style.top = savedPos.top+'px';
    panel.style.right = 'unset';
  }
  panel.querySelector('#ua-panel-header').addEventListener('mousedown',e=>{
    isDragging=true; offsetX=e.clientX-panel.offsetLeft; offsetY=e.clientY-panel.offsetTop;
  });
  document.addEventListener('mouseup',()=>isDragging=false);
  document.addEventListener('mousemove',e=>{
    if(isDragging){
      panel.style.left=(e.clientX-offsetX)+'px';
      panel.style.top=(e.clientY-offsetY)+'px';
      panel.style.right='unset';
    }
  });
  window.addEventListener('beforeunload',()=>{
    localStorage.setItem('uaPanelPos',JSON.stringify({left:panel.offsetLeft,top:panel.offsetTop}));
  });

  // === скрыть ===
  panel.querySelector('#ua-hide').addEventListener('click',()=>{
    panel.style.display='none';
    localStorage.setItem('uaPanelHidden','true');
  });
  if(localStorage.getItem('uaPanelHidden')==='true') panel.style.display='none';

  // === Chart.js загрузился ===
  chartScript.onload = ()=>init();

  function init(){
    const ctx=document.getElementById('ua-chart').getContext('2d');
    let chart=new Chart(ctx,{type:'bar',data:{labels:[],datasets:[{label:'ТОП-10',data:[],backgroundColor:[]}]}});

    function randomColor(){
      return `hsl(${Math.floor(Math.random()*360)},70%,50%)`;
    }

    function collectMessages(){
      const msgs=document.querySelectorAll('[class*="message"]');
      let arr=[];
      msgs.forEach(m=>{
        const text=m.innerText||'';
        const date=m.querySelector('time')?.getAttribute('datetime')||'';
        const reactions=m.querySelectorAll('[class*="reaction"]').length;
        const media=m.querySelectorAll('img,video').length;
        arr.push({el:m,text,date,reactions,media});
      });
      return arr;
    }

    function updateChart(data){
      chart.data.labels=data.slice(0,10).map((_,i)=>i+1);
      chart.data.datasets[0].data=data.slice(0,10).map(d=>d.value);
      chart.data.datasets[0].backgroundColor=data.slice(0,10).map(()=>randomColor());
      chart.update();
    }

    function sortData(type){
      let msgs=collectMessages();
      let now=Date.now();
      if(type==='24h') msgs=msgs.filter(m=>now-(new Date(m.date).getTime())<86400000);
      let data=[];
      switch(type){
        case 'reactions':
          data=msgs.map(m=>({value:m.reactions,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'date_desc':
          data=msgs.map(m=>({value:new Date(m.date).getTime(),text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'date_asc':
          data=msgs.map(m=>({value:new Date(m.date).getTime(),text:m.text})).sort((a,b)=>a.value-b.value);
          break;
        case 'media':
          data=msgs.map(m=>({value:m.media,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        case 'length':
          data=msgs.map(m=>({value:m.text.length,text:m.text})).sort((a,b)=>b.value-a.value);
          break;
        default:
          data=msgs.map(m=>({value:m.text.length,text:m.text}));
      }
      updateChart(data);
    }

    document.querySelectorAll('#ua-buttons button').forEach(btn=>{
      btn.addEventListener('click',()=>{
        const sort=btn.dataset.sort;
        const action=btn.dataset.action;
        if(sort) sortData(sort);
        if(action==='24h') sortData('24h');
        if(action==='refresh') sortData('reactions');
        if(action==='csv'){
          const msgs=collectMessages();
          let csv='Текст;Реакции;Медиа;Дата\n';
          msgs.forEach(m=>csv+=`"${m.text.replace(/"/g,'""')}";${m.reactions};${m.media};${m.date}\n`);
          const blob=new Blob([csv],{type:'text/csv'});
          const a=document.createElement('a');
          a.href=URL.createObjectURL(blob);
          a.download='telegram_export.csv';
          a.click();
        }
      });
    });

    // === языковая смена ===
    const langSelect = panel.querySelector('#ua-lang select');
    langSelect.addEventListener('change',()=>{
      const lang = langSelect.value;
      localStorage.setItem('uaPanelLang', lang);
      applyLang(lang);
    });
    const savedLang = localStorage.getItem('uaPanelLang') || 'ua';
    langSelect.value = savedLang;
    applyLang(savedLang);

    function applyLang(lang){
      const translations = {
        ua: { reactions:'За реакціями', date_desc:'Дата ↓', date_asc:'Дата ↑', media:'По медіа', length:'По довжині', '24h':'За 24г', refresh:'Оновити', csv:'Експорт CSV' },
        ru: { reactions:'По реакциям', date_desc:'Дата ↓', date_asc:'Дата ↑', media:'По медиа', length:'По длине', '24h':'За 24ч', refresh:'Обновить', csv:'Экспорт CSV' },
        en: { reactions:'By reactions', date_desc:'Date ↓', date_asc:'Date ↑', media:'By media', length:'By length', '24h':'Last 24h', refresh:'Refresh', csv:'Export CSV' },
        de: { reactions:'Nach Reaktionen', date_desc:'Datum ↓', date_asc:'Datum ↑', media:'Nach Medien', length:'Nach Länge', '24h':'Letzte 24h', refresh:'Aktualisieren', csv:'CSV exportieren' },
        fr: { reactions:'Par réactions', date_desc:'Date ↓', date_asc:'Date ↑', media:'Par médias', length:'Par longueur', '24h':'24h', refresh:'Rafraîchir', csv:'Exporter CSV' },
        es: { reactions:'Por reacciones', date_desc:'Fecha ↓', date_asc:'Fecha ↑', media:'Por medios', length:'Por longitud', '24h':'Últimas 24h', refresh:'Actualizar', csv:'Exportar CSV' },
        it: { reactions:'Per reazioni', date_desc:'Data ↓', date_asc:'Data ↑', media:'Per media', length:'Per lunghezza', '24h':'Ultime 24h', refresh:'Aggiorna', csv:'Esporta CSV' },
        pt: { reactions:'Por reações', date_desc:'Data ↓', date_asc:'Data ↑', media:'Por mídia', length:'Por comprimento', '24h':'Últimas 24h', refresh:'Atualizar', csv:'Exportar CSV' },
        pl: { reactions:'Według reakcji', date_desc:'Data ↓', date_asc:'Data ↑', media:'Według mediów', length:'Według długości', '24h':'Ostatnie 24h', refresh:'Odśwież', csv:'Eksport CSV' },
        zh: { reactions:'按反应', date_desc:'日期 ↓', date_asc:'日期 ↑', media:'按媒体', length:'按长度', '24h':'24小时内', refresh:'刷新', csv:'导出 CSV' },
        ja: { reactions:'リアクション順', date_desc:'日付 ↓', date_asc:'日付 ↑', media:'メディア順', length:'長さ順', '24h':'過去24時間', refresh:'更新', csv:'CSVエクスポート' },
        ko: { reactions:'반응순', date_desc:'날짜 ↓', date_asc:'날짜 ↑', media:'미디어순', length:'길이순', '24h':'최근 24시간', refresh:'새로고침', csv:'CSV 내보내기' }
      };
      document.querySelectorAll('#ua-buttons button').forEach(btn=>{
        const key = btn.dataset.sort || btn.dataset.action;
        if(translations[lang][key]) btn.textContent = translations[lang][key];
      });
    }

    const chatContainer=document.querySelector('#column-center')||document.body;
    if(chatContainer){
      const observer=new MutationObserver(()=>sortData('reactions'));
      observer.observe(chatContainer,{childList:true,subtree:true});
    }

    setTimeout(()=>sortData('reactions'),3000);
  }
})();