Fullchan X

16/04/2025, 18:06:52

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

  1. // ==UserScript==
  2. // @name Fullchan X
  3. // @namespace Violentmonkey Scripts
  4. // @match https://8chan.moe/*/res/*.html*
  5. // @grant none
  6. // @version 1.1.1
  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. this.handleClick(event);
  52. });
  53. }
  54. }
  55.  
  56. handleClick (event) {
  57. const clicked = event.target;
  58.  
  59. const post = clicked.closest('.innerPost') || clicked.closest('.innerOP');
  60. if (!post) return;
  61.  
  62. const isNested = !!post.closest('.nestedPost');
  63. const nestQuote = clicked.closest('.quoteLink');
  64. const postMedia = clicked.closest('a[data-filemime]');
  65.  
  66. if (nestQuote) {
  67. event.preventDefault();
  68. this.nestQuote(nestQuote);
  69. } else if (postMedia && isNested) {
  70. this.handleMediaClick(event, postMedia);
  71. }
  72. }
  73.  
  74. handleMediaClick (event, postMedia) {
  75. if (postMedia.dataset.filemime === "video/webm") return;
  76. event.preventDefault();
  77. const imageSrc = `${postMedia.href}`;
  78. const imageEl = postMedia.querySelector('img');
  79. if (!postMedia.dataset.thumbSrc) postMedia.dataset.thumbSrc = `${imageEl.src}`;
  80.  
  81. const isExpanding = imageEl.src !== imageSrc;
  82.  
  83. if (isExpanding) {
  84. imageEl.src = imageSrc;
  85. imageEl.classList
  86. }
  87. imageEl.src = isExpanding ? imageSrc : postMedia.dataset.thumbSrc;
  88. imageEl.classList.toggle('imgExpanded', isExpanding);
  89. }
  90.  
  91. assignPostOrder () {
  92. const postOrderReplies = (post) => {
  93. const replyCount = post.querySelectorAll('.panelBacklinks a').length;
  94. post.style.order = 100 - replyCount;
  95. }
  96.  
  97. const postOrderCatbox = (post) => {
  98. const postContent = post.querySelector('.divMessage').textContent;
  99. const matches = postContent.match(/catbox\.moe/g);
  100. const catboxCount = matches ? matches.length : 0;
  101. post.style.order = 100 - catboxCount;
  102. }
  103.  
  104. if (this.postOrder === 'default') {
  105. this.thread.style.display = 'block';
  106. return;
  107. }
  108.  
  109. this.thread.style.display = 'flex';
  110.  
  111. if (this.postOrder === 'replies') {
  112. this.posts.forEach(post => postOrderReplies(post));
  113. } else if (this.postOrder === 'catbox') {
  114. this.posts.forEach(post => postOrderCatbox(post));
  115. }
  116. }
  117.  
  118. updateYous () {
  119. const yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
  120. const yousLinks = yous.map(you => {
  121. const youLink = document.createElement('a');
  122. youLink.textContent = '>>' + you.id;
  123. youLink.href = '#' + you.id;
  124. return youLink;
  125. })
  126.  
  127. this.yousContainer.innerHTML = '';
  128. yousLinks.forEach(you => this.yousContainer.appendChild(you));
  129. }
  130.  
  131. nestQuote(quoteLink) {
  132. const parentPostMessage = quoteLink.closest('.divMessage');
  133. const quoteId = quoteLink.href.split('#')[1];
  134. const quotePost = document.getElementById(quoteId);
  135. if (!quotePost) return;
  136.  
  137. const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
  138. if (!quotePostContent) return;
  139.  
  140. const existing = parentPostMessage.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
  141. if (existing) {
  142. existing.remove();
  143. return;
  144. }
  145.  
  146. const wrapper = document.createElement('div');
  147. wrapper.classList.add('nestedPost');
  148. wrapper.setAttribute('data-quote-id', quoteId);
  149.  
  150. const clone = quotePostContent.cloneNode(true);
  151. clone.style.whiteSpace = "unset";
  152. wrapper.appendChild(clone);
  153.  
  154. parentPostMessage.appendChild(wrapper);
  155. }
  156. };
  157.  
  158. window.customElements.define('fullchan-x', fullChanX);
  159.  
  160.  
  161.  
  162. // Create fullchan-x elemnt
  163. const fcx = document.createElement('fullchan-x');
  164. fcx.innerHTML = `
  165. <div class="fcx__controls">
  166. <select id="thread-sort">
  167. <option value="default">Default</option>
  168. <option value="replies">Replies</option>
  169. <option value="catbox">Catbox</option>
  170. </select>
  171.  
  172. <div class="fcx__my-yous">
  173. <p class="my-yous__label">My (You)s</p>
  174. <div class="my-yous__yous" id="my-yous"></div>
  175. </div>
  176. </div>
  177. `;
  178. document.body.appendChild(fcx);
  179. fcx.init();
  180.  
  181.  
  182.  
  183. // Styles
  184. const style = document.createElement('style');
  185. style.innerHTML = `
  186. fullchan-x {
  187. display: block;
  188. position: fixed;
  189. top: 2.5rem;
  190. right: 2rem;
  191. padding: 10px;
  192. background: var(--contrast-color);
  193. border: 1px solid var(--navbar-text-color);
  194. color: var(--link-color);
  195. font-size: 14px;
  196. opacity: 0.5;
  197. }
  198.  
  199. fullchan-x:hover {
  200. opacity: 1;
  201. }
  202.  
  203. .divPosts {
  204. flex-direction: column;
  205. }
  206.  
  207. .fcx__controls {
  208. display: flex;
  209. flex-direction: column;
  210. gap: 2px;
  211. }
  212.  
  213. #thread-sort {
  214. padding: 0.4rem 0.6rem;
  215. background: white !important;
  216. border: none !important;
  217. border-radius: 0.2rem;
  218. transition: all ease 150ms;
  219. cursor: pointer;
  220. }
  221.  
  222. .my-yous__yous {
  223. display: none;
  224. flex-direction: column;
  225. }
  226.  
  227. .my-yous__label {
  228. padding: 0.4rem 0.6rem;
  229. background: white !important;
  230. border: none !important;
  231. border-radius: 0.2rem;
  232. transition: all ease 150ms;
  233. cursor: pointer;
  234. }
  235.  
  236. .fcx__my-yous:hover .my-yous__yous {
  237. display: flex;
  238. }
  239.  
  240. .innerPost:has(.quoteLink.you) {
  241. border-left: solid #dd003e 6px;
  242. }
  243.  
  244. .innerPost:has(.youName) {
  245. border-left: solid #68b723 6px;
  246. }
  247.  
  248. // --- Nested quotes ----
  249. // I don't know why it needs this, weird CSS inheritance on cloned element
  250. .nestedPost {}
  251. .divMessage .nestedPost {
  252. display: block;
  253. white-space: normal!important;
  254. overflow-wrap: anywhere;
  255. margin-top: 0.5em;
  256. border: 1px solid var(--navbar-text-color);
  257. }
  258.  
  259. .nestedPost .innerPost,
  260. .nestedPost .innerOP {
  261. width: 100%;
  262. }
  263.  
  264. .nestedPost .imgLink .imgExpanded {
  265. width: auto!important;
  266. height: auto!important;
  267. }
  268. `;
  269.  
  270. document.head.appendChild(style);
  271.  
  272.  
  273. // Asuka and Eris (fantasy Asuka) are best girls