2ch tree post fork

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

当前为 2025-03-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 2ch tree post fork
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description делает треды древовидными, добавляет сворачивание веток и подсветку новых
  6. // @author You
  7. // @match http://2ch.hk/*/res/*
  8. // @match https://2ch.hk/*/res/*
  9. // @match http://2ch.life/*/res/*
  10. // @match https://2ch.life/*/res/*
  11. // @grant none
  12. // @grant GM_addStyle
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. "use strict";
  20. console.time("tree script");
  21.  
  22. // Вспомогательные функции
  23.  
  24. // Добавляет CSS стили
  25. function addStyle(css) {
  26. const style = document.createElement('style');
  27. style.type = 'text/css';
  28. style.textContent = css;
  29. document.head.appendChild(style);
  30. }
  31.  
  32. // Получает номер поста из элемента
  33. function getPostNumber(postElement) {
  34. if (!postElement) return null;
  35. const id = postElement.id; // "post-123456"
  36. return parseInt(id.replace("post-", ""));
  37. }
  38. // Перемещает пост и применяет стили
  39. function postMove(linkPost, isNewPost = false) {
  40. const nodePostCurr = linkPost.parentNode.parentNode; // Текущий пост (обертка .post)
  41. const postNumber = linkPost.innerText.match(/\d+/);
  42.  
  43. if (!postNumber) return; // Если не удалось извлечь номер, выходим
  44.  
  45. const targetPostNumber = postNumber[0];
  46.  
  47. // Проверяем, ссылка на OP, другой тред или несуществующий пост
  48. if (/OP|→/.test(linkPost.innerText)) {
  49. return;
  50. }
  51. const nodePostReply = document.querySelector(`#post-${targetPostNumber}`);
  52. if (!nodePostReply) {
  53. //console.warn(`Target post #${targetPostNumber} not found.`); // отладка, если пост не найден
  54. return;
  55. }
  56.  
  57. // Добавляем класс, помечающий что в посте есть ответы (для сворачивания)
  58. if (!nodePostReply.classList.contains('has-replies')) {
  59. nodePostReply.classList.add('has-replies');
  60.  
  61. // Добавляем кнопку сворачивания
  62. const collapseButton = document.createElement('span');
  63. collapseButton.classList.add('collapse-button');
  64. collapseButton.textContent = '[-]';
  65. collapseButton.title = "Свернуть/Развернуть ветку";
  66. // Добавляем обработчик сворачивания/разворачивания
  67. collapseButton.addEventListener('click', (event) => {
  68. event.stopPropagation(); // Предотвращаем всплытие, чтобы клик по кнопке не выделял пост
  69. const replies = nodePostReply.querySelectorAll(':scope > .post'); // :scope - только непосредственные дочерние .post
  70. replies.forEach(reply => {
  71. reply.classList.toggle('collapsed');
  72. });
  73. collapseButton.textContent = collapseButton.textContent === '[-]' ? '[+]' : '[-]'; // Меняем текст кнопки
  74. });
  75.  
  76. // Вставляем кнопку сворачивания перед .post__details
  77. const postDetails = nodePostReply.querySelector('.post__details');
  78. if (postDetails) {
  79. postDetails.parentNode.insertBefore(collapseButton, postDetails);
  80. }
  81. }
  82.  
  83. nodePostReply.append(nodePostCurr); // Перемещаем
  84.  
  85.  
  86. // Подсветка новых постов
  87. if (isNewPost) {
  88. nodePostCurr.classList.add('new-post'); // Добавляем класс для новых
  89. // Убираем подсветку при клике (однократно)
  90. nodePostCurr.addEventListener("click", () => {
  91. nodePostCurr.classList.remove('new-post');
  92. nodePostCurr.style["border-left"] = "2px dashed"; // Добавляем dashed border при клике
  93. }, { once: true });
  94. }
  95.  
  96. }
  97.  
  98. // --- Основная логика ---
  99.  
  100. // 1. Обработка существующих постов
  101. const initialLinks = document.querySelectorAll(`.post__message > :nth-child(1)[data-num]`);
  102. initialLinks.forEach(postMove);
  103.  
  104. // 2. Наблюдение за новыми постами
  105. const threadContainer = document.querySelector(".thread");
  106.  
  107. const observer = new MutationObserver((mutations) => {
  108. mutations.forEach((mutation) => {
  109. if (mutation.addedNodes.length > 0) {
  110. mutation.addedNodes.forEach(addedNode => {
  111. // Проверяем, что добавленный узел - это пост (у него есть класс .post)
  112. if (addedNode.classList && addedNode.classList.contains('post')) {
  113. const newLink = addedNode.querySelector(`.post__message > :nth-child(1)[data-num]`);
  114. if (newLink) {
  115. postMove(newLink, true);
  116. }
  117. }
  118.  
  119. });
  120. }
  121. });
  122. });
  123.  
  124. // 3. Запускаем наблюдение
  125. observer.observe(threadContainer, { childList: true });
  126.  
  127.  
  128. // 4. Стили
  129. addStyle(`
  130. .post .post_type_reply {
  131. border-left: 2px solid white; /* Исходный цвет границы */
  132. margin-left: 5px; /* небольшой отступ */
  133. padding-left: 5px;
  134. }
  135. .new-post {
  136. border-left-color: yellow !important; /* Подсветка новых постов */
  137. }
  138.  
  139. .post.collapsed {
  140. display: none;
  141. }
  142. .collapse-button{
  143. cursor: pointer;
  144. margin-right: 5px;
  145. color: #888; /* Серый цвет */
  146. }
  147. .has-replies{
  148. position: relative; /* Для позиционирования кнопки */
  149. }
  150.  
  151.  
  152. `);
  153.  
  154.  
  155. console.timeEnd("tree script");
  156. })();