2ch tree post fork

делает треды древовидными, добавляет сворачивание веток и подсветку новых

// ==UserScript==
// @name         2ch tree post fork
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  делает треды древовидными, добавляет сворачивание веток и подсветку новых
// @author       You
// @match        http://2ch.hk/*/res/*
// @match        https://2ch.hk/*/res/*
// @match        http://2ch.life/*/res/*
// @match        https://2ch.life/*/res/*
// @grant        none
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license MIT
// ==/UserScript==

(function () {
  "use strict";
  console.time("tree script");

  // Вспомогательные функции

  // Добавляет CSS стили
  function addStyle(css) {
    const style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = css;
    document.head.appendChild(style);
  }

  // Получает номер поста из элемента
  function getPostNumber(postElement) {
      if (!postElement) return null;
      const id = postElement.id; // "post-123456"
      return parseInt(id.replace("post-", ""));
  }
  
  // Перемещает пост и применяет стили
  function postMove(linkPost, isNewPost = false) {
    const nodePostCurr = linkPost.parentNode.parentNode;  // Текущий пост (обертка .post)
    const postNumber = linkPost.innerText.match(/\d+/);

    if (!postNumber) return; // Если не удалось извлечь номер, выходим

    const targetPostNumber = postNumber[0];

    // Проверяем, ссылка на OP, другой тред или несуществующий пост
    if (/OP|→/.test(linkPost.innerText)) {
      return;
    }
      
    const nodePostReply = document.querySelector(`#post-${targetPostNumber}`);
    if (!nodePostReply) {
        //console.warn(`Target post #${targetPostNumber} not found.`); // отладка, если пост не найден
        return;
    }

      // Добавляем класс, помечающий что в посте есть ответы (для сворачивания)
      if (!nodePostReply.classList.contains('has-replies')) {
          nodePostReply.classList.add('has-replies');

          // Добавляем кнопку сворачивания
          const collapseButton = document.createElement('span');
          collapseButton.classList.add('collapse-button');
          collapseButton.textContent = '[-]';
          collapseButton.title = "Свернуть/Развернуть ветку";
          
          // Добавляем обработчик сворачивания/разворачивания
          collapseButton.addEventListener('click', (event) => {
              event.stopPropagation(); // Предотвращаем всплытие, чтобы клик по кнопке не выделял пост
              const replies = nodePostReply.querySelectorAll(':scope > .post'); // :scope - только непосредственные дочерние .post
              replies.forEach(reply => {
                  reply.classList.toggle('collapsed');
              });
              collapseButton.textContent = collapseButton.textContent === '[-]' ? '[+]' : '[-]'; // Меняем текст кнопки
          });

          // Вставляем кнопку сворачивания перед .post__details
          const postDetails = nodePostReply.querySelector('.post__details');
          if (postDetails) {
             postDetails.parentNode.insertBefore(collapseButton, postDetails);
          }
          
      }

    nodePostReply.append(nodePostCurr); // Перемещаем


      // Подсветка новых постов
    if (isNewPost) {
      nodePostCurr.classList.add('new-post'); // Добавляем класс для новых
        // Убираем подсветку при клике (однократно)
      nodePostCurr.addEventListener("click", () => {
        nodePostCurr.classList.remove('new-post');
        nodePostCurr.style["border-left"] = "2px dashed"; // Добавляем dashed border при клике
      }, { once: true });
    }

  }
    

  // --- Основная логика ---

  // 1. Обработка существующих постов
  const initialLinks = document.querySelectorAll(`.post__message > :nth-child(1)[data-num]`);
  initialLinks.forEach(postMove);

  // 2. Наблюдение за новыми постами
  const threadContainer = document.querySelector(".thread");

  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.addedNodes.length > 0) {
        mutation.addedNodes.forEach(addedNode => {
            // Проверяем, что добавленный узел - это пост (у него есть класс .post)
          if (addedNode.classList && addedNode.classList.contains('post')) {
              const newLink = addedNode.querySelector(`.post__message > :nth-child(1)[data-num]`);
              if (newLink) {
                  postMove(newLink, true);
              }
          }

        });
      }
    });
  });

    // 3. Запускаем наблюдение
  observer.observe(threadContainer, { childList: true });


    // 4. Стили
  addStyle(`
    .post .post_type_reply {
      border-left: 2px solid white; /* Исходный цвет границы */
      margin-left: 5px; /* небольшой отступ */
       padding-left: 5px;
    }
    .new-post {
      border-left-color: yellow !important; /* Подсветка новых постов */
    }

     .post.collapsed {
        display: none;
     }
     .collapse-button{
        cursor: pointer;
        margin-right: 5px;
        color: #888; /* Серый цвет */
     }
     .has-replies{
        position: relative; /* Для позиционирования кнопки */
     }


  `);


  console.timeEnd("tree script");
})();