Fullchan X

16/04/2025, 18:06:52

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

  1. // ==UserScript==
  2. // @name Fullchan X
  3. // @namespace Violentmonkey Scripts
  4. // @match https://8chan.moe/*/res/*.html*
  5. // @grant none
  6. // @version 1.0
  7. // @author vfyxe
  8. // @description 16/04/2025, 18:06:52
  9. // ==/UserScript==
  10.  
  11. if (!document.querySelector('.divPosts')) return;
  12.  
  13. class fullChanX extends HTMLElement {
  14. constructor() {
  15. super();
  16. this.enableNestedQuotes = true;
  17. }
  18.  
  19. init() {
  20. this.threadParent = document.querySelector('#divThreads');
  21. this.thread = this.threadParent.querySelector('.divPosts');
  22. this.posts = [...this.thread.querySelectorAll('.postCell')];
  23. this.postOrder = 'default';
  24. this.postOrderSelect = this.querySelector('#thread-sort');
  25. this.yousContainer = this.querySelector('#my-yous');
  26. this.updateYous();
  27. this.observers();
  28. }
  29.  
  30. observers () {
  31. this.postOrderSelect.addEventListener('change', (event) => {
  32. this.postOrder = event.target.value;
  33. this.assignPostOrder();
  34. });
  35.  
  36. const observerCallback = (mutationsList, observer) => {
  37. for (const mutation of mutationsList) {
  38. if (mutation.type === 'childList') {
  39. this.posts = [...this.thread.querySelectorAll('.postCell')];
  40. if (this.postOrder !== 'default') this.assignPostOrder();
  41. this.updateYous();
  42. }
  43. }
  44. };
  45.  
  46. const threadObserver = new MutationObserver(observerCallback);
  47. threadObserver.observe(this.thread, { childList: true, subtree: false });
  48.  
  49. if (this.enableNestedQuotes) {
  50. this.thread.addEventListener('click', event => {
  51. const nestQuote = event.target.closest('.quoteLink');
  52. if (nestQuote) {
  53. event.preventDefault();
  54. this.nestQuote(nestQuote);
  55. }
  56. });
  57. }
  58. }
  59.  
  60. assignPostOrder () {
  61. const postOrderReplies = (post) => {
  62. const replyCount = post.querySelectorAll('.panelBacklinks a').length;
  63. post.style.order = 100 - replyCount;
  64. }
  65.  
  66. const postOrderCatbox = (post) => {
  67. const postContent = post.querySelector('.divMessage').textContent;
  68. const matches = postContent.match(/catbox\.moe/g);
  69. const catboxCount = matches ? matches.length : 0;
  70. post.style.order = 100 - catboxCount;
  71. }
  72.  
  73. if (this.postOrder === 'default') {
  74. this.thread.style.display = 'block';
  75. return;
  76. }
  77.  
  78. this.thread.style.display = 'flex';
  79.  
  80. if (this.postOrder === 'replies') {
  81. this.posts.forEach(post => postOrderReplies(post));
  82. } else if (this.postOrder === 'catbox') {
  83. this.posts.forEach(post => postOrderCatbox(post));
  84. }
  85. }
  86.  
  87. updateYous () {
  88. const yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
  89. const yousLinks = yous.map(you => {
  90. const youLink = document.createElement('a');
  91. youLink.textContent = '>>' + you.id;
  92. youLink.href = '#' + you.id;
  93. return youLink;
  94. })
  95.  
  96. this.yousContainer.innerHTML = '';
  97. yousLinks.forEach(you => this.yousContainer.appendChild(you));
  98. }
  99.  
  100. nestQuote(quoteLink) {
  101. const parentPostMessage = quoteLink.closest('.divMessage');
  102. const quoteId = quoteLink.href.split('#')[1];
  103. const quotePost = document.getElementById(quoteId);
  104. if (!quotePost) return;
  105.  
  106. const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
  107. if (!quotePostContent) return;
  108.  
  109. const existing = parentPostMessage.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
  110. if (existing) {
  111. existing.remove();
  112. return;
  113. }
  114.  
  115. const wrapper = document.createElement('div');
  116. wrapper.classList.add('nestedPost');
  117. wrapper.setAttribute('data-quote-id', quoteId);
  118.  
  119. const clone = quotePostContent.cloneNode(true);
  120. clone.style.whiteSpace = "unset";
  121. wrapper.appendChild(clone);
  122.  
  123. parentPostMessage.appendChild(wrapper);
  124. }
  125. };
  126.  
  127. window.customElements.define('fullchan-x', fullChanX);
  128.  
  129.  
  130.  
  131. // Create fullchan-x elemnt
  132. const fcx = document.createElement('fullchan-x');
  133. fcx.innerHTML = `
  134. <div class="fcx__controls">
  135. <select id="thread-sort">
  136. <option value="default">Default</option>
  137. <option value="replies">Replies</option>
  138. <option value="catbox">Catbox</option>
  139. </select>
  140.  
  141. <div class="fcx__my-yous">
  142. <p class="my-yous__label">My (You)s</p>
  143. <div class="my-yous__yous" id="my-yous"></div>
  144. </div>
  145. </div>
  146. `;
  147. document.body.appendChild(fcx);
  148. fcx.init();
  149.  
  150.  
  151.  
  152. // Styles
  153. const style = document.createElement('style');
  154. style.innerHTML = `
  155. fullchan-x {
  156. display: block;
  157. position: fixed;
  158. top: 2.5rem;
  159. right: 2rem;
  160. padding: 10px;
  161. background: var(--contrast-color);
  162. border: 1px solid var(--navbar-text-color);
  163. color: var(--link-color);
  164. font-size: 14px;
  165. opacity: 0.5;
  166. }
  167.  
  168. fullchan-x:hover {
  169. opacity: 1;
  170. }
  171.  
  172. .divPosts {
  173. flex-direction: column;
  174. }
  175.  
  176. .fcx__controls {
  177. display: flex;
  178. flex-direction: column;
  179. gap: 2px;
  180. }
  181.  
  182. #thread-sort {
  183. padding: 0.4rem 0.6rem;
  184. background: white !important;
  185. border: none !important;
  186. border-radius: 0.2rem;
  187. transition: all ease 150ms;
  188. cursor: pointer;
  189. }
  190.  
  191. .my-yous__yous {
  192. display: none;
  193. flex-direction: column;
  194. }
  195.  
  196. .my-yous__label {
  197. padding: 0.4rem 0.6rem;
  198. background: white !important;
  199. border: none !important;
  200. border-radius: 0.2rem;
  201. transition: all ease 150ms;
  202. cursor: pointer;
  203. }
  204.  
  205. .fcx__my-yous:hover .my-yous__yous {
  206. display: flex;
  207. }
  208.  
  209. .innerPost:has(.quoteLink.you) {
  210. border-left: solid red 6px;
  211. }
  212.  
  213. // --- Nested quotes ----
  214. // I don't know why it needs this, weird CSS inheritance on cloned element
  215. .nestedPost {}
  216. .divMessage .nestedPost {
  217. display: block;
  218. white-space: normal!important;
  219. overflow-wrap: anywhere;
  220. margin-top: 0.5em;
  221. border: 1px solid var(--navbar-text-color);
  222. }
  223. `;
  224.  
  225. document.head.appendChild(style);
  226.  
  227.  
  228. // Asuka and Eris (fantasy Asuka) are best girls