2ch tree post fork

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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");
})();