FutabaX

自動更新、引用元に引用アンカー追加、マウスオーバーで画像拡大表示などお役立ち機能を追加するスクリプト

  1. // ==UserScript==
  2. // @name FutabaX
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description 自動更新、引用元に引用アンカー追加、マウスオーバーで画像拡大表示などお役立ち機能を追加するスクリプト
  6. // @author としあき
  7. // @match *://*.2chan.net/*
  8. // @match https://www.2chan.net/index2.html
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_addStyle
  11. // @connect 2chan.net
  12. // @connect futakuro.com
  13. // @connect futabaforest.net
  14. // @connect ftbucket.info
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. GM_addStyle(`
  22. td.rtd {
  23. white-space: normal !important;
  24. word-wrap: break-word !important;
  25. overflow-wrap: break-word !important;
  26. text-align: left !important;
  27. }
  28. a[data-quote-post] {
  29. display: inline-block !important;
  30. white-space: nowrap;
  31. margin: 2px 5px 2px 0 !important;
  32. }
  33. `);
  34.  
  35. let resolvedThreadCache = {};
  36.  
  37. if (window.top === window.self && document.getElementsByTagName("frameset").length > 0) {
  38. window.location.replace("https://www.2chan.net/index2.html");
  39. }
  40.  
  41. if (window.location.href.indexOf("index2.html") !== -1) {
  42. function modifyFrontPageLinks() {
  43. document.querySelectorAll('a[href="https://dec.2chan.net/dec/futaba.htm"]').forEach(a => {
  44. a.textContent = "二次元裏dec";
  45. });
  46. document.querySelectorAll('a[href="https://jun.2chan.net/jun/futaba.htm"]').forEach(a => {
  47. a.textContent = "二次元裏jun";
  48. });
  49. document.querySelectorAll('a[href="https://may.2chan.net/b/futaba.htm"]').forEach(a => {
  50. a.textContent = "二次元裏may";
  51. });
  52.  
  53. const mayLink = document.querySelector('a[href="https://may.2chan.net/b/futaba.htm"]');
  54. if (mayLink) {
  55. mayLink.insertAdjacentHTML("afterend",
  56. '<br><a href="https://img.2chan.net/b/futaba.htm">二次元裏img</a><br>' +
  57. '<a href="https://cgi.2chan.net/b/futaba.htm">二次元裏cgi</a><br>' +
  58. '<a href="https://dat.2chan.net/b/futaba.htm">二次元裏dat</a>'
  59. );
  60. }
  61.  
  62. document.querySelectorAll('a[href="https://may.2chan.net/b/futaba.htm"]').forEach(a => {
  63. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  64. });
  65. document.querySelectorAll('a[href="https://img.2chan.net/b/futaba.htm"]').forEach(a => {
  66. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  67. });
  68. document.querySelectorAll('a[href="https://dec.2chan.net/84/futaba.htm"]').forEach(a => {
  69. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  70. });
  71. document.querySelectorAll('a[href="https://dec.2chan.net/60/futaba.htm"]').forEach(a => {
  72. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  73. });
  74. document.querySelectorAll('a[href="https://dec.2chan.net/55/futaba.htm"]').forEach(a => {
  75. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  76. });
  77. document.querySelectorAll('a[href="https://dec.2chan.net/73/futaba.htm"]').forEach(a => {
  78. a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>');
  79. });
  80. }
  81. modifyFrontPageLinks();
  82. }
  83.  
  84. const originalFavicon = "https://2chan.net/favicon.ico";
  85. const thresholdFaviconBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAfdJREFUOE9VU1tuE0EQrCYSHAHtjpcbxJYT4QsAAhtfAfgg/Od1C4Jk/hBxfnwFZAfkPUGQgpScINrMOGcwynbSj7UTyVrvztR0dVfVUF3XTASA5UeQd2YGZA3NH4Ft13CKNwwx1yu8bDQgOy4V5Kit2xn70icxSM4LyBYfsMi7EpKwWCsCa1qTTy0g/MyYDfuIi4Q8z/D61Rs82z1cccmMcn45+opyXiIlweUYTP+ARIPZ8B1aoQCeAPGqUpbwIqDzY+zMhOX3I5TzOZgJRRF0vaqiFGA+2dpEluV6sBWCzlstKhRZgc2fx/g/+oayLBFaQce6rqLKk2ICmYTAbPAW6SYhPM/BG1boOka8/3WK8XYHve2XqBYRdAvElBBaGQbT31KgZlVY5QKmwz5SjG4nodfr4e/ZmTJyzQghoD87NV9U/7p2uUXpBz6DsRwdYTKZ4OOHT3i6dwDiRyaqxTqCOaSTWEpkw7s66bbx+fwCRJYDDY87KuXUBTXJkyeSKECtY4y7Hez8u/B0mp1GZSSaRGFdRWldXpHjrTa+nF9qaATXxH7VrLqg4zch9ynAOO62bf79Qy1mGEujRVmmdRE0rk2DTqMrrpDdC78POq4EWEZYt+BX0udUWawrbf/RLXDbpYDkoPG0kdiY16bYNXRfPDVa/h50B6WqJRL7N7kGAAAAAElFTkSuQmCC";
  86. const newReplyFaviconBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRF////R7RHLZoti/iLY9Bj/wAAERW9LQAAAFFJREFUeJxjZIACRiwMIca3DMz8/xgZnc/qHzB8rMvIxG984b2g416g1H+Dg/Yiu4GKhQQUHhivBuliVrgbCmYwKDzAxWDSuwAVQbILLhK6GgDWTBmEbxfYVgAAAABJRU5ErkJggg==";
  87. let thresholdMode = false;
  88.  
  89. const extraStyle = document.createElement("style");
  90. extraStyle.innerHTML = `
  91. .highlighted {
  92. background-color: #EDD0BD !important;
  93. transition: background-color 2s ease-out;
  94. }
  95. .quote-preview {
  96. background-color: #F0E0D6;
  97. padding: 8px;
  98. border: 1px solid #F0E0D6;
  99. max-width: 90vw;
  100. overflow: auto;
  101. box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
  102. }
  103. #replyContainer {
  104. position: fixed;
  105. bottom: 20px;
  106. right: 20px;
  107. background: #fff;
  108. border: 1px solid #ccc;
  109. padding: 5px;
  110. z-index: 15000;
  111. box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
  112. }
  113. #replyDragHandle {
  114. background: #ccc;
  115. padding: 4px;
  116. cursor: move;
  117. font-size: 0.9em;
  118. text-align: center;
  119. user-select: none;
  120. }
  121. `;
  122. document.head.appendChild(extraStyle);
  123.  
  124. function highlightPost(post) {
  125. post.classList.add("highlighted");
  126. setTimeout(() => {
  127. post.classList.remove("highlighted");
  128. }, 2000);
  129. }
  130.  
  131. let currentQuotePreview = null;
  132. function handleQuoteLinkMouseEnter(e) {
  133. const link = this;
  134. const quoteNum = link.dataset.quotePost;
  135. const target = document.getElementById("post-" + quoteNum);
  136. if (!target) return;
  137. currentQuotePreview = target.cloneNode(true);
  138. currentQuotePreview.classList.add("quote-preview");
  139. currentQuotePreview.style.position = "fixed";
  140. currentQuotePreview.style.zIndex = "20000";
  141. currentQuotePreview.style.left = (e.clientX + 10) + "px";
  142. currentQuotePreview.style.top = (e.clientY - 10) + "px";
  143. document.body.appendChild(currentQuotePreview);
  144. }
  145. function handleQuoteLinkMouseMove(e) {
  146. if (currentQuotePreview) {
  147. currentQuotePreview.style.left = (e.clientX + 10) + "px";
  148. currentQuotePreview.style.top = (e.clientY - 10) + "px";
  149. }
  150. }
  151. function handleQuoteLinkMouseLeave(e) {
  152. if (currentQuotePreview) {
  153. currentQuotePreview.remove();
  154. currentQuotePreview = null;
  155. }
  156. }
  157.  
  158. function removeUnwantedElements(root = document) {
  159. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/sphead.htm"], iframe[src^="https://dec.2chan.net/bin/spfoot_a.htm"]').forEach(iframe => {
  160. if (iframe.parentElement) { iframe.parentElement.remove(); }
  161. });
  162. root.querySelectorAll('div').forEach(div => {
  163. if (div.innerHTML.includes("ヘッダ広告ここから")) { div.remove(); }
  164. });
  165. root.querySelectorAll('.tue2').forEach(el => el.remove());
  166. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/overlay.htm"]').forEach(iframe => {
  167. if (iframe.parentElement) {
  168. let parent = iframe.parentElement;
  169. parent.remove();
  170. let prev = parent.previousElementSibling;
  171. if (prev && prev.tagName === 'DIV' && prev.getAttribute('style') && prev.getAttribute('style').includes('height:68px')) {
  172. prev.remove();
  173. }
  174. }
  175. });
  176. root.querySelectorAll('style').forEach(styleEl => {
  177. if (styleEl.innerHTML.includes('.footfix')) { styleEl.remove(); }
  178. });
  179. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/foot2_a.htm"]').forEach(iframe => {
  180. if (iframe.parentElement) { iframe.parentElement.remove(); }
  181. });
  182. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/hsi1.htm"], iframe[src^="https://dec.2chan.net/bin/foot4_ab.htm"]').forEach(iframe => {
  183. if (iframe.parentElement) { iframe.parentElement.remove(); }
  184. });
  185. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/hsif.htm"]').forEach(iframe => {
  186. if (iframe.parentElement) { iframe.parentElement.remove(); }
  187. });
  188. root.querySelectorAll('iframe[src^="/bin/catp.htm"]').forEach(iframe => {
  189. let container = iframe.closest('div[style*="width:610px"]');
  190. if (container) { container.remove(); }
  191. else if (iframe.parentElement) { iframe.parentElement.remove(); }
  192. });
  193. root.querySelectorAll('.footfix').forEach(el => el.remove());
  194. ["imobile_adspotdiv1", "imobile_adspotdiv2"].forEach(id => {
  195. const el = document.getElementById(id);
  196. if (el) { el.style.display = "none"; }
  197. });
  198. root.querySelectorAll('div[class^="adWrapper"]').forEach(el => el.remove());
  199. root.querySelectorAll('#rightadc').forEach(el => el.remove());
  200. root.querySelectorAll('div#rightadfloat').forEach(el => el.remove());
  201. root.querySelectorAll('div.heaven-728-90.ninja-slider.mode-b.dark').forEach(el => el.remove());
  202. root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/foot1_n.htm"]').forEach(iframe => {
  203. if (iframe.parentElement) { iframe.parentElement.remove(); }
  204. });
  205.  
  206. root.querySelectorAll('div[id*="ad" i]').forEach(el => el.remove());
  207.  
  208. root.querySelectorAll('div[style*="width:680px"][style*="margin: 0 auto"]').forEach(el => el.remove());
  209. }
  210.  
  211.  
  212.  
  213. function cleanseJumpLinks(root = document) {
  214. const jumpLinks = root.querySelectorAll('a[href^="/bin/jump.php?"]');
  215. jumpLinks.forEach(link => {
  216. let href = link.getAttribute('href');
  217. let cleaned = href.replace(/^\/bin\/jump\.php\?/, '');
  218. link.setAttribute('href', cleaned);
  219. });
  220. }
  221.  
  222. function isSrcLink(anchor) {
  223. return /\.(png|jpe?g|gif|mp4|webm)$/i.test(anchor.href);
  224. }
  225.  
  226. function addDownloadButtons(root = document) {
  227. const allAnchors = Array.from(
  228. root.querySelectorAll('a[href*="/src/"], a[href*="fu"]')
  229. ).filter(isSrcLink);
  230.  
  231. const filenameAnchors = allAnchors.filter(anchor => !anchor.querySelector('img'));
  232.  
  233. filenameAnchors.forEach(anchor => {
  234. if (anchor.nextElementSibling && anchor.nextElementSibling.classList?.contains('download-btn')) {
  235. return;
  236. }
  237.  
  238. const downloadLink = document.createElement('a');
  239. downloadLink.href = anchor.href;
  240. downloadLink.download = '';
  241. downloadLink.className = 'download-btn';
  242. downloadLink.style.display = 'inline';
  243. downloadLink.style.verticalAlign = 'middle';
  244. downloadLink.style.marginLeft = '5px';
  245.  
  246. if (/\.(mp4|webm)$/i.test(downloadLink.href)) {
  247. downloadLink.addEventListener('click', function(e) {
  248. e.preventDefault();
  249. fetch(downloadLink.href)
  250. .then(resp => resp.blob())
  251. .then(blob => {
  252. const blobUrl = window.URL.createObjectURL(blob);
  253. const a = document.createElement('a');
  254. a.href = blobUrl;
  255. a.download = downloadLink.href.split('/').pop();
  256. document.body.appendChild(a);
  257. a.click();
  258. a.remove();
  259. window.URL.revokeObjectURL(blobUrl);
  260. })
  261. .catch(err => console.error("Download failed", err));
  262. });
  263. }
  264. const svgNS = "http://www.w3.org/2000/svg";
  265. const svg = document.createElementNS(svgNS, "svg");
  266. svg.setAttribute("xmlns", svgNS);
  267. svg.setAttribute("viewBox", "0 0 512 512");
  268. svg.setAttribute("aria-hidden", "true");
  269. svg.setAttribute("focusable", "false");
  270. svg.style.width = "1em";
  271. svg.style.height = "1em";
  272. svg.style.verticalAlign = "middle";
  273. svg.style.fill = "currentColor";
  274.  
  275. const path = document.createElementNS(svgNS, "path");
  276. path.setAttribute("d", "M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 242.7-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7 288 32zM64 352c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-101.5 0-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352 64 352zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z");
  277. svg.appendChild(path);
  278. downloadLink.appendChild(svg);
  279.  
  280. anchor.insertAdjacentElement('afterend', downloadLink);
  281.  
  282. });
  283. }
  284. function processPost(td) {
  285. const bq = td.querySelector("blockquote");
  286. if (!bq) return;
  287.  
  288. const originalHTML = bq.innerHTML;
  289. const lines = originalHTML.split(/<br\s*\/?>/i);
  290.  
  291. const imageFiles = new Set();
  292. const videoFiles = new Set();
  293.  
  294. lines.forEach(line => {
  295. const div = document.createElement("div");
  296. div.innerHTML = line;
  297. const text = div.textContent.trim();
  298.  
  299. if (text.startsWith(">") || text.startsWith("&gt;")) return;
  300.  
  301. const matches = text.match(/\b(fu?\d+\.\w+)\b/gi);
  302. if (!matches) return;
  303.  
  304. matches.forEach(fullname => {
  305. const ext = (fullname.split(".").pop() || "").toLowerCase();
  306. if (ext === "mp4" || ext === "webm") {
  307. videoFiles.add(fullname);
  308. } else {
  309. imageFiles.add(fullname);
  310. }
  311. });
  312. });
  313.  
  314. if (imageFiles.size > 0) {
  315. td.querySelectorAll("a[href*='/up/src/'], a[href*='/up2/src/']").forEach(a => {
  316. if (!bq.contains(a)) {
  317. a.remove();
  318. }
  319. });
  320.  
  321. const container = document.createElement("div");
  322. container.style.margin = "0";
  323. container.style.padding = "0";
  324. container.style.display = "block";
  325.  
  326. Array.from(imageFiles).forEach((filename, index, arr) => {
  327. const baseUrl = filename.toLowerCase().startsWith("fu")
  328. ? "https://dec.2chan.net/up2/src/"
  329. : "https://dec.2chan.net/up/src/";
  330. const imageUrl = baseUrl + filename;
  331.  
  332. const thumbLink = document.createElement("a");
  333. thumbLink.href = imageUrl;
  334. thumbLink.target = "_blank";
  335.  
  336. const img = document.createElement("img");
  337. img.src = imageUrl;
  338. img.setAttribute("border", "0");
  339. img.setAttribute("align", "left");
  340. img.setAttribute("hspace", "20");
  341. img.setAttribute("alt", "外部画像");
  342. img.setAttribute("loading", "lazy");
  343. img.style.maxWidth = "200px";
  344. img.style.maxHeight = "200px";
  345.  
  346. thumbLink.appendChild(img);
  347. container.appendChild(thumbLink);
  348. if (index < arr.length - 1) {
  349. container.appendChild(document.createElement("br"));
  350. }
  351. });
  352.  
  353. td.insertBefore(container, bq);
  354. bq.style.marginLeft = "241px";
  355. }
  356.  
  357. let replacedHTML = originalHTML;
  358.  
  359. replacedHTML = replacedHTML.replace(
  360. /\b(fu?\d+\.(mp4|webm))\b/gi,
  361. (match, filename) => {
  362. const baseUrl = filename.toLowerCase().startsWith("fu")
  363. ? "https://dec.2chan.net/up2/src/"
  364. : "https://dec.2chan.net/up/src/";
  365. const videoUrl = baseUrl + filename;
  366.  
  367. return `<a href="${videoUrl}" target="_blank">${filename}</a>` +
  368. ` <span class="embed-video-link" data-url="${videoUrl}" style="cursor:pointer;">(動画)</span>`;
  369. }
  370. );
  371.  
  372. replacedHTML = replacedHTML.replace(
  373. /\b(fu?\d+\.(?!mp4|webm)\w+)\b/gi,
  374. (match, filename) => {
  375. const baseUrl = filename.toLowerCase().startsWith("fu")
  376. ? "https://dec.2chan.net/up2/src/"
  377. : "https://dec.2chan.net/up/src/";
  378. const imageUrl = baseUrl + filename;
  379. return `<a href="${imageUrl}" target="_blank">${filename}</a>`;
  380. }
  381. );
  382.  
  383. bq.innerHTML = replacedHTML;
  384.  
  385. bq.querySelectorAll(".embed-video-link").forEach(el => {
  386. el.addEventListener("click", function() {
  387. if (this.nextSibling && this.nextSibling.className === "video-iframe-wrap") {
  388. this.nextSibling.remove();
  389. return;
  390. }
  391.  
  392. const iframeWrap = document.createElement("div");
  393. iframeWrap.className = "video-iframe-wrap";
  394. iframeWrap.style.position = "relative";
  395. iframeWrap.style.marginTop = "5px";
  396.  
  397. const closeBtn = document.createElement("div");
  398. closeBtn.textContent = "✖";
  399. closeBtn.style.position = "absolute";
  400. closeBtn.style.top = "3px";
  401. closeBtn.style.right = "5px";
  402. closeBtn.style.cursor = "pointer";
  403. closeBtn.style.backgroundColor = "rgba(0,0,0,0.6)";
  404. closeBtn.style.color = "#fff";
  405. closeBtn.style.padding = "2px 6px";
  406. closeBtn.style.borderRadius = "3px";
  407. closeBtn.style.zIndex = "10";
  408. closeBtn.onclick = () => iframeWrap.remove();
  409.  
  410. const iframe = document.createElement("iframe");
  411. iframe.src = this.dataset.url;
  412. iframe.style.width = "400px";
  413. iframe.style.height = "300px";
  414. iframe.style.border = "1px solid #ccc";
  415. iframe.style.borderRadius = "3px";
  416.  
  417. iframeWrap.appendChild(closeBtn);
  418. iframeWrap.appendChild(iframe);
  419. this.parentNode.insertBefore(iframeWrap, this.nextSibling);
  420. });
  421. });
  422. }
  423.  
  424. function updateFileSizes(root = document) {
  425. const elements = root.querySelectorAll('.thre, .rtd');
  426. elements.forEach(el => {
  427. el.innerHTML = el.innerHTML.replace(/(-\()(\d+)\s*B\)/g, (match, prefix, num) => {
  428. let kb = (parseInt(num, 10) / 1024).toFixed(1);
  429. return prefix + kb + " KB)";
  430. });
  431. });
  432. }
  433.  
  434. function attachFullSizeHover(root = document) {
  435. const allAnchors = Array.from(root.querySelectorAll('a[href*="/src/"], a[href*="2chan.net/up2/src/"]')).filter(anchor => {
  436. return /\.(jpg|jpeg|png|gif)$/i.test(anchor.href);
  437. });
  438.  
  439. const thumbnails = allAnchors
  440. .filter(anchor => anchor.querySelector('img'))
  441. .map(anchor => anchor.querySelector('img'));
  442.  
  443. thumbnails.forEach(img => {
  444. if (img.dataset.fullsizeHoverAttached) return;
  445. img.addEventListener('mouseenter', e => { showFullSizePreview(e.currentTarget); });
  446. img.addEventListener('mouseleave', e => { hideFullSizePreview(); });
  447. img.dataset.fullsizeHoverAttached = "true";
  448. });
  449. }
  450. function showFullSizePreview(img) {
  451. hideFullSizePreview();
  452. const fullSrc = img.parentElement.href;
  453. const preview = document.createElement('img');
  454. preview.id = 'fullsize-preview';
  455. preview.src = fullSrc;
  456. preview.style.position = 'fixed';
  457. preview.style.top = '50%';
  458. preview.style.left = '50%';
  459. preview.style.transform = 'translate(-50%, -50%)';
  460. preview.style.maxWidth = '90vw';
  461. preview.style.maxHeight = '90vh';
  462. preview.style.zIndex = '2147483647';
  463. preview.style.border = '1px solid #ccc';
  464. preview.style.pointerEvents = 'none';
  465. document.body.appendChild(preview);
  466. }
  467. function hideFullSizePreview() {
  468. const preview = document.getElementById('fullsize-preview');
  469. if (preview) preview.remove();
  470. }
  471.  
  472. function autoReloadThread() {
  473. if (document.title.includes("404 File Not Found")) return;
  474. const reloadAnchor = document.querySelector('#contres a');
  475. if (reloadAnchor) {
  476. const onClickAttr = reloadAnchor.getAttribute('onClick');
  477. const match = onClickAttr && onClickAttr.match(/scrlf\((\d+)\)/);
  478. if (match) {
  479. const threadId = match[1];
  480. if (typeof unsafeWindow.scrlf === 'function') {
  481. unsafeWindow.scrlf(threadId);
  482. }
  483. }
  484. }
  485. }
  486. setInterval(autoReloadThread, 5000);
  487.  
  488. function changeFavicon(newIconData) {
  489. let link = document.querySelector("link[rel*='icon']");
  490. if (!link) {
  491. link = document.createElement("link");
  492. link.rel = "icon";
  493. document.head.appendChild(link);
  494. }
  495. link.href = newIconData;
  496. }
  497.  
  498. function changeFaviconIfCondition() {
  499. if (thresholdMode) return;
  500. const maxresElement = document.querySelector('span.maxres');
  501. const contdispElement = document.querySelector('#contdisp');
  502. if ((maxresElement && maxresElement.textContent.includes("上限1000レスに達しました")) ||
  503. (contdispElement && contdispElement.textContent.includes("スレッドがありません"))) {
  504. changeFavicon(thresholdFaviconBase64);
  505. thresholdMode = true;
  506. }
  507. }
  508. setInterval(changeFaviconIfCondition, 1000);
  509.  
  510. function checkSodCondition() {
  511. if (document.title.includes("404 File Not Found")) return;
  512. const sodElements = document.querySelectorAll('.sod');
  513. sodElements.forEach(el => {
  514. if (el.textContent.trim() === "そうだねx0" && !el.dataset.sodAlerted) {
  515. alert("お使いのipアドレスからそうだねできません");
  516. el.dataset.sodAlerted = "true";
  517. }
  518. });
  519. }
  520. setInterval(checkSodCondition, 1000);
  521.  
  522. function addQuoteLinks() {
  523. if (document.title.includes("404 File Not Found")) return;
  524. const allPosts = Array.from(document.querySelectorAll('.thre, .rtd'))
  525. .filter(post => post.querySelector('span.cno'));
  526. let postDict = {};
  527. allPosts.forEach(post => {
  528. const cnoEl = post.querySelector('span.cno');
  529. if (cnoEl) {
  530. const num = cnoEl.textContent.trim().replace(/^No\./, '');
  531. if (!post.id) { post.id = "post-" + num; }
  532. post.dataset.postNumber = num;
  533. const isOP = !post.closest('table') && post.textContent.includes('画像ファイル名');
  534. if (isOP) {
  535. post.dataset.isOP = "true";
  536. }
  537. postDict[num] = post;
  538. }
  539. });
  540. const replyPosts = allPosts.filter(post => post.closest("table") !== null);
  541. let quotes = [];
  542. replyPosts.forEach(post => {
  543. const quotePostNum = post.dataset.postNumber;
  544. const fonts = Array.from(post.querySelectorAll('blockquote font[color="#789922"]'));
  545. fonts.forEach(font => {
  546. let text = font.textContent.trim();
  547. text = text.replace(/^>+/, '').replace(/^(&gt;)+/, '').trim();
  548. if (text.indexOf('\n') !== -1) {
  549. text = text.split('\n')[0].trim();
  550. }
  551. if (!text) return;
  552. let type = "text";
  553. let value = text;
  554. const numMatch = text.match(/^(?:No\.?\s*)?(\d{8,})$/);
  555. if (numMatch) {
  556. type = "number";
  557. value = numMatch[1];
  558. } else if (text.match(/\.(jpg|jpeg|png|gif|webp|mp4|webm)$/i)) {
  559. type = "filename";
  560. value = text;
  561. }
  562. quotes.push({ quotePostNum, type, value, sourcePost: post });
  563. });
  564. });
  565. quotes.sort((a, b) => parseInt(a.quotePostNum) - parseInt(b.quotePostNum));
  566. allPosts.forEach(originalPost => {
  567. const origPostNum = originalPost.dataset.postNumber;
  568. const isOP = originalPost.dataset.isOP === "true";
  569. let lines = [];
  570. let anchors = [];
  571. let originalFilename = '';
  572. if (isOP) {
  573. const filenameAnchor = originalPost.querySelector('a[href*="/src/"]');
  574. if (filenameAnchor) {
  575. originalFilename = filenameAnchor.textContent.trim();
  576. }
  577. const block = originalPost.querySelector('blockquote');
  578. if (block) {
  579. lines = block.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
  580. }
  581. } else {
  582. const block = originalPost.querySelector('blockquote');
  583. if (block) {
  584. lines = block.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
  585. } else {
  586. lines = originalPost.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0);
  587. }
  588. anchors = Array.from(originalPost.querySelectorAll('a[href*="/src/"]'));
  589. }
  590. let insertAfterElement;
  591. if (isOP) {
  592. insertAfterElement = originalPost.querySelector('.cntd');
  593. } else {
  594. insertAfterElement = originalPost.querySelector('.sod');
  595. }
  596. if (!insertAfterElement) return;
  597. for (let i = quotes.length - 1; i >= 0; i--) {
  598. let quote = quotes[i];
  599. if (quote.sourcePost.id === originalPost.id) continue;
  600. let matchFound = false;
  601. if (quote.type === "number") {
  602. matchFound = (origPostNum === quote.value);
  603. } else if (quote.type === "filename") {
  604. if (isOP) {
  605. matchFound = (originalFilename === quote.value);
  606. } else {
  607. matchFound = anchors.some(a =>
  608. a.getAttribute('href').includes(quote.value) ||
  609. a.textContent.trim() === quote.value
  610. );
  611. }
  612. } else {
  613. matchFound = lines.includes(quote.value);
  614. }
  615. if (matchFound) {
  616. if (!originalPost.querySelector(`a[data-quote-post="${quote.quotePostNum}"]`)) {
  617. const link = document.createElement('a');
  618. link.href = "javascript:void(0);";
  619. link.textContent = ">>" + quote.quotePostNum;
  620. link.style.marginLeft = "5px";
  621. link.dataset.quotePost = quote.quotePostNum;
  622. link.addEventListener('click', () => {
  623. const target = document.getElementById("post-" + quote.quotePostNum);
  624. if (target) {
  625. target.scrollIntoView({ behavior: "smooth" });
  626. highlightPost(target);
  627. }
  628. });
  629. link.addEventListener('mouseenter', handleQuoteLinkMouseEnter);
  630. link.addEventListener('mousemove', handleQuoteLinkMouseMove);
  631. link.addEventListener('mouseleave', handleQuoteLinkMouseLeave);
  632. insertAfterElement.parentNode.insertBefore(link, insertAfterElement.nextSibling);
  633. }
  634. }
  635. }
  636. });
  637. }
  638. setInterval(addQuoteLinks, 1000);
  639.  
  640.  
  641. function insertQuoteBreaks() {
  642. var posts = document.querySelectorAll('td.rtd');
  643. posts.forEach(function(post) {
  644. var quotes = post.querySelectorAll('a[data-quote-post]');
  645. quotes.forEach(function(quote, index) {
  646. if ((index + 1) % 10 === 0) {
  647. if (!quote.nextSibling || quote.nextSibling.nodeName !== 'BR') {
  648. quote.insertAdjacentHTML('afterend', '<br>');
  649. }
  650. }
  651. });
  652. });
  653. }
  654.  
  655. function makeDraggable(element, handle) {
  656. handle = handle || element;
  657. handle.style.cursor = "move";
  658. let offsetX, offsetY;
  659. handle.addEventListener("mousedown", function(e) {
  660. offsetX = e.clientX - element.getBoundingClientRect().left;
  661. offsetY = e.clientY - element.getBoundingClientRect().top;
  662. document.addEventListener("mousemove", moveHandler);
  663. document.addEventListener("mouseup", upHandler);
  664. });
  665. function moveHandler(e) {
  666. element.style.left = (e.clientX - offsetX) + "px";
  667. element.style.top = (e.clientY - offsetY) + "px";
  668. element.style.bottom = "auto";
  669. element.style.right = "auto";
  670. }
  671. function upHandler(e) {
  672. document.removeEventListener("mousemove", moveHandler);
  673. document.removeEventListener("mouseup", upHandler);
  674. }
  675. }
  676. function setupReplyForm() {
  677. const replyForm = document.getElementById("fm");
  678. if (replyForm) {
  679. const reszbElements = replyForm.querySelectorAll("span#reszb");
  680. reszbElements.forEach(el => el.remove());
  681. const ftb2 = replyForm.querySelector("table.ftb2");
  682. if (ftb2) {
  683. ftb2.parentNode.removeChild(ftb2);
  684. const hrElement = document.querySelector("hr");
  685. if (hrElement) {
  686. hrElement.insertAdjacentElement("afterend", ftb2);
  687. } else {
  688. document.body.insertBefore(ftb2, document.body.firstChild);
  689. }
  690. }
  691. const container = document.createElement("div");
  692. container.id = "replyContainer";
  693. container.style.position = "fixed";
  694. container.style.bottom = "20px";
  695. container.style.right = "20px";
  696. container.style.background = "#fff";
  697. container.style.border = "1px solid #ccc";
  698. container.style.padding = "5px";
  699. container.style.zIndex = "15000";
  700. container.style.boxShadow = "2px 2px 10px rgba(0,0,0,0.5)";
  701. const handle = document.createElement("div");
  702. handle.id = "replyDragHandle";
  703. handle.textContent = "返信フォーム(ドラッグで移動)";
  704. handle.style.background = "#ccc";
  705. handle.style.padding = "4px";
  706. handle.style.cursor = "move";
  707. handle.style.fontSize = "0.9em";
  708. handle.style.textAlign = "center";
  709. handle.style.userSelect = "none";
  710. container.appendChild(handle);
  711. container.appendChild(replyForm);
  712. document.body.appendChild(container);
  713. makeDraggable(container, handle);
  714. }
  715. }
  716. if (window.location.href.indexOf("/res/") !== -1) {
  717. if (document.readyState === "loading") {
  718. document.addEventListener("DOMContentLoaded", setupReplyForm);
  719. } else {
  720. setupReplyForm();
  721. }
  722. }
  723.  
  724. function addArchiveLinksTo404() {
  725. if (!document.title.includes("404 File Not Found")) return;
  726. let h1s = document.querySelectorAll("h1");
  727. let targetH1 = null;
  728. h1s.forEach(h => {
  729. if (h.textContent.includes("掲示板に戻る")) { targetH1 = h; }
  730. });
  731. if (!targetH1) return;
  732. let hr = targetH1.nextElementSibling;
  733. while (hr && hr.tagName !== "HR") {
  734. hr = hr.nextElementSibling;
  735. }
  736. if (!hr) return;
  737. let archiveHeader = document.createElement("h1");
  738. archiveHeader.textContent = "過去ログを取得";
  739. hr.insertAdjacentElement("afterend", archiveHeader);
  740. let linkContainer = document.createElement("div");
  741. linkContainer.style.marginTop = "10px";
  742. let path = window.location.pathname;
  743. let regex = /\/([^\/]+)\/res\/(\d+)\.htm/i;
  744. let match = path.match(regex);
  745. if (!match) return;
  746. let board = match[1];
  747. let threadId = match[2];
  748. let host = window.location.host;
  749. let links = [];
  750. if (host.includes("may.2chan.net") && board === "b") {
  751. links.push({ text: "futakuro", url: `https://kako.futakuro.com/futa/may_b/${threadId}/` });
  752. links.push({ text: "ftbucket (may1)", url: `https://may1.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` });
  753. links.push({ text: "ftbucket (may2)", url: `https://may2.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` });
  754. links.push({ text: "ftbucket (may3)", url: `https://may3.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` });
  755. links.push({ text: "futabaforest", url: `https://futabaforest.net/b/res/${threadId}.htm` });
  756. } else if (host.includes("img.2chan.net") && board === "b") {
  757. links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/img/cont/img.2chan.net_b_res_${threadId}/index.htm` });
  758. links.push({ text: "futakuro", url: `https://kako.futakuro.com/futa/img_b/${threadId}/` });
  759. } else if (host.includes("jun.2chan.net") && board === "jun") {
  760. links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/jun/cont/jun.2chan.net_jun_res_${threadId}/index.htm` });
  761. } else if (host.includes("dec.2chan.net") && board === "55") {
  762. links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/dec55/cont/dec.2chan.net_55_res_${threadId}/index.htm` });
  763. } else if (host.includes("dec.2chan.net") && board === "60") {
  764. links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/dec60/cont/dec.2chan.net_60_res_${threadId}/index.htm` });
  765. } else {
  766. links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/other/cont/${host}_${board}_res_${threadId}/index.htm` });
  767. }
  768. links.forEach(linkInfo => {
  769. let a = document.createElement("a");
  770. a.href = linkInfo.url;
  771. a.textContent = linkInfo.text;
  772. a.style.marginRight = "10px";
  773. a.target = "_blank";
  774. linkContainer.appendChild(a);
  775. });
  776. archiveHeader.insertAdjacentElement("afterend", linkContainer);
  777. }
  778. addArchiveLinksTo404();
  779.  
  780. const originalTitle = document.title;
  781. let maxSeenIndex = -1;
  782. let prevUnread = 0;
  783. let initialUnread = 0;
  784. let initialUnreadSet = false;
  785. let wasZeroOnBlur = false;
  786. function isInViewport(el) {
  787. const rect = el.getBoundingClientRect();
  788. return rect.bottom > 0 && rect.top < window.innerHeight;
  789. }
  790. function updateUnreadCountNew() {
  791. if (window.location.href.indexOf("/res/") === -1) return;
  792. if (document.title.includes("404 File Not Found")) {
  793. changeFavicon(thresholdFaviconBase64);
  794. return;
  795. }
  796. const contdispElement = document.querySelector('#contdisp');
  797. if (contdispElement && contdispElement.textContent.includes("スレッドがありません")) {
  798. document.title = `【落ち】 ${originalTitle}`;
  799. return;
  800. }
  801. const replies = Array.from(document.querySelectorAll(".thre, .rtd"))
  802. .filter(post => post.closest("table") !== null);
  803. const totalReplies = replies.length;
  804. let visibleIndex = -1;
  805. for (let i = 0; i < replies.length; i++) {
  806. if (isInViewport(replies[i])) {
  807. visibleIndex = i;
  808. }
  809. }
  810. if (document.hasFocus()) {
  811. if (visibleIndex > maxSeenIndex) {
  812. maxSeenIndex = visibleIndex;
  813. }
  814. if (!initialUnreadSet) {
  815. initialUnread = totalReplies - (maxSeenIndex + 1);
  816. initialUnreadSet = true;
  817. }
  818. } else {
  819. if (!initialUnreadSet) {
  820. maxSeenIndex = totalReplies - 1;
  821. initialUnread = 0;
  822. initialUnreadSet = true;
  823. }
  824. }
  825. const unread = totalReplies - (maxSeenIndex + 1);
  826. document.title = `(${unread}) ${originalTitle}`;
  827. if (!thresholdMode && !document.hasFocus() && wasZeroOnBlur && prevUnread === 0 && unread > 0) {
  828. changeFavicon(newReplyFaviconBase64);
  829. }
  830. if (document.hasFocus() && unread === 0 && !thresholdMode) {
  831. changeFavicon(originalFavicon);
  832. }
  833. prevUnread = unread;
  834. }
  835. window.addEventListener("scroll", updateUnreadCountNew);
  836. setInterval(updateUnreadCountNew, 1000);
  837. window.addEventListener("blur", () => {
  838. if (prevUnread === 0) { wasZeroOnBlur = true; }
  839. });
  840. window.addEventListener("focus", () => {
  841. wasZeroOnBlur = false;
  842. if (prevUnread === 0 && !thresholdMode) { changeFavicon(originalFavicon); }
  843. });
  844.  
  845. function addYouTubeEmbeds(root = document) {
  846. const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([\w-]{11})/;
  847. const processedLinks = new Set();
  848.  
  849. function createFloatingWindow(videoId) {
  850. const floatDiv = document.createElement('div');
  851. floatDiv.style.cssText = `
  852. position: fixed;
  853. width: 480px;
  854. height: 270px;
  855. z-index: 9999;
  856. background: white;
  857. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  858. overflow: hidden;
  859. cursor: move;
  860. `;
  861.  
  862. const handle = document.createElement('div');
  863. handle.style.cssText = `
  864. position: absolute;
  865. top: 0;
  866. left: 0;
  867. right: 0;
  868. height: 20px;
  869. background: #eee0d7;
  870. cursor: move;
  871. user-select: none;
  872. `;
  873.  
  874. const closeButton = document.createElement('button');
  875. closeButton.textContent = 'X';
  876. closeButton.style.cssText = `
  877. position: absolute;
  878. top: 0;
  879. right: 0;
  880. padding: 2px 6px;
  881. background: #781208;
  882. color: white;
  883. border: none;
  884. cursor: pointer;
  885. z-index: 10000;
  886. `;
  887. closeButton.addEventListener('click', () => floatDiv.remove());
  888.  
  889. const iframe = document.createElement('iframe');
  890. iframe.style.cssText = `
  891. width: 100%;
  892. height: calc(100% - 20px);
  893. border: none;
  894. margin-top: 20px;
  895. `;
  896. iframe.src = `https://www.youtube.com/embed/${videoId}?rel=0&wmode=opaque`;
  897. iframe.setAttribute('allow', 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture');
  898. iframe.setAttribute('allowfullscreen', '');
  899.  
  900. handle.appendChild(closeButton);
  901. floatDiv.appendChild(handle);
  902. floatDiv.appendChild(iframe);
  903.  
  904. const leftHandle = document.createElement('div');
  905. leftHandle.style.cssText = `
  906. position: absolute;
  907. top: 20px;
  908. left: 0;
  909. width: 8px;
  910. height: calc(100% - 20px);
  911. cursor: ew-resize;
  912. z-index: 10001;
  913. `;
  914. const rightHandle = document.createElement('div');
  915. rightHandle.style.cssText = `
  916. position: absolute;
  917. top: 20px;
  918. right: 0;
  919. width: 8px;
  920. height: calc(100% - 20px);
  921. cursor: ew-resize;
  922. z-index: 10001;
  923. `;
  924.  
  925. const bottomHandle = document.createElement('div');
  926. bottomHandle.style.cssText = `
  927. position: absolute;
  928. bottom: 0;
  929. left: 0;
  930. height: 8px;
  931. width: 100%;
  932. cursor: ns-resize;
  933. z-index: 10001;
  934. `;
  935.  
  936. floatDiv.appendChild(leftHandle);
  937. floatDiv.appendChild(rightHandle);
  938. floatDiv.appendChild(bottomHandle);
  939. document.body.appendChild(floatDiv);
  940.  
  941. const startX = (window.innerWidth - 480) / 2;
  942. const startY = (window.innerHeight - 270) / 2;
  943. floatDiv.style.left = `${startX}px`;
  944. floatDiv.style.top = `${startY}px`;
  945.  
  946. let isDragging = false;
  947. let startXPos = 0;
  948. let startYPos = 0;
  949. let initialMouseX = 0;
  950. let initialMouseY = 0;
  951.  
  952. handle.addEventListener('mousedown', startDrag);
  953. document.addEventListener('mousemove', drag);
  954. document.addEventListener('mouseup', stopDrag);
  955.  
  956. function startDrag(e) {
  957. isDragging = true;
  958. initialMouseX = e.clientX;
  959. initialMouseY = e.clientY;
  960. startXPos = floatDiv.offsetLeft;
  961. startYPos = floatDiv.offsetTop;
  962. floatDiv.style.cursor = 'grabbing';
  963. }
  964.  
  965. function drag(e) {
  966. if (isDragging) {
  967. const deltaX = e.clientX - initialMouseX;
  968. const deltaY = e.clientY - initialMouseY;
  969. floatDiv.style.left = `${startXPos + deltaX}px`;
  970. floatDiv.style.top = `${startYPos + deltaY}px`;
  971. }
  972. }
  973.  
  974. function stopDrag() {
  975. isDragging = false;
  976. floatDiv.style.cursor = 'move';
  977. }
  978.  
  979. leftHandle.addEventListener('mousedown', function(e) {
  980. e.preventDefault();
  981. e.stopPropagation();
  982. let startX = e.clientX;
  983. let startLeft = floatDiv.offsetLeft;
  984. let startWidth = floatDiv.offsetWidth;
  985. function doResize(ev) {
  986. let deltaX = ev.clientX - startX;
  987. let newWidth = startWidth - deltaX;
  988. let newLeft = startLeft + deltaX;
  989. if(newWidth < 200) {
  990. newWidth = 200;
  991. newLeft = startLeft + (startWidth - 200);
  992. }
  993. floatDiv.style.width = newWidth + 'px';
  994. floatDiv.style.left = newLeft + 'px';
  995. }
  996. function stopResize() {
  997. document.removeEventListener('mousemove', doResize);
  998. document.removeEventListener('mouseup', stopResize);
  999. }
  1000. document.addEventListener('mousemove', doResize);
  1001. document.addEventListener('mouseup', stopResize);
  1002. });
  1003.  
  1004. rightHandle.addEventListener('mousedown', function(e) {
  1005. e.preventDefault();
  1006. e.stopPropagation();
  1007. let startX = e.clientX;
  1008. let startWidth = floatDiv.offsetWidth;
  1009. function doResize(ev) {
  1010. let deltaX = ev.clientX - startX;
  1011. let newWidth = startWidth + deltaX;
  1012. if(newWidth < 200) {
  1013. newWidth = 200;
  1014. }
  1015. floatDiv.style.width = newWidth + 'px';
  1016. }
  1017. function stopResize() {
  1018. document.removeEventListener('mousemove', doResize);
  1019. document.removeEventListener('mouseup', stopResize);
  1020. }
  1021. document.addEventListener('mousemove', doResize);
  1022. document.addEventListener('mouseup', stopResize);
  1023. });
  1024.  
  1025. bottomHandle.addEventListener('mousedown', function(e) {
  1026. e.preventDefault();
  1027. e.stopPropagation();
  1028. let startY = e.clientY;
  1029. let startHeight = floatDiv.offsetHeight;
  1030. function doResize(ev) {
  1031. let deltaY = ev.clientY - startY;
  1032. let newHeight = startHeight + deltaY;
  1033. if(newHeight < 150) {
  1034. newHeight = 150;
  1035. }
  1036. floatDiv.style.height = newHeight + 'px';
  1037. }
  1038. function stopResize() {
  1039. document.removeEventListener('mousemove', doResize);
  1040. document.removeEventListener('mouseup', stopResize);
  1041. }
  1042. document.addEventListener('mousemove', doResize);
  1043. document.addEventListener('mouseup', stopResize);
  1044. });
  1045. }
  1046.  
  1047. function processLink(link) {
  1048. if (processedLinks.has(link)) return;
  1049. const match = link.href.match(ytRegex);
  1050. if (!match) return;
  1051. const videoId = match[1];
  1052. const containerId = `yt-${videoId}-${Date.now()}`;
  1053.  
  1054. const toggle = document.createElement('a');
  1055. toggle.href = "javascript:void(0);";
  1056. toggle.textContent = "(埋め込み)";
  1057. toggle.style.marginLeft = "5px";
  1058.  
  1059. const floatButton = document.createElement('a');
  1060. floatButton.href = "javascript:void(0);";
  1061. floatButton.textContent = "(フロート表示)";
  1062. floatButton.style.marginLeft = "5px";
  1063.  
  1064. const container = document.createElement('div');
  1065. container.id = containerId;
  1066. container.style.cssText = `
  1067. margin: 10px 0;
  1068. position: relative;
  1069. width: 100%;
  1070. max-width: 640px;
  1071. display: none;
  1072. `;
  1073.  
  1074. const iframe = document.createElement('iframe');
  1075. iframe.style.cssText = `
  1076. width: 100%;
  1077. height: 360px;
  1078. border: none;
  1079. border-radius: 4px;
  1080. `;
  1081. iframe.src = `https://www.youtube.com/embed/${videoId}?rel=0&wmode=opaque`;
  1082. iframe.setAttribute('allow', 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture');
  1083. iframe.setAttribute('allowfullscreen', '');
  1084.  
  1085. toggle.addEventListener('click', (e) => {
  1086. e.preventDefault();
  1087. container.style.display = container.style.display === 'none' ? 'block' : 'none';
  1088. iframe.src = iframe.src;
  1089. });
  1090.  
  1091. floatButton.addEventListener('click', (e) => {
  1092. e.preventDefault();
  1093. createFloatingWindow(videoId);
  1094. });
  1095.  
  1096. container.appendChild(iframe);
  1097. link.parentNode.insertBefore(toggle, link.nextSibling);
  1098. link.parentNode.insertBefore(floatButton, toggle.nextSibling);
  1099. link.parentNode.insertBefore(container, floatButton.nextSibling);
  1100. processedLinks.add(link);
  1101. }
  1102.  
  1103. root.querySelectorAll('a[href*="youtube"], a[href*="youtu.be"]').forEach(processLink);
  1104.  
  1105. const observer = new MutationObserver(mutations => {
  1106. mutations.forEach(mutation => {
  1107. mutation.addedNodes.forEach(node => {
  1108. if (node.nodeType === 1) {
  1109. node.querySelectorAll('a[href*="youtube"], a[href*="youtu.be"]').forEach(processLink);
  1110. }
  1111. });
  1112. });
  1113. });
  1114. observer.observe(root, { childList: true, subtree: true });
  1115. }
  1116.  
  1117.  
  1118.  
  1119. function resolveOtherThreadLinks(root = document) {
  1120. if (window.location.href.indexOf("/res/") === -1) return;
  1121.  
  1122. const candidateLinks = root.querySelectorAll('a[href$=".htm"]');
  1123. candidateLinks.forEach(link => {
  1124. if (link.closest('#contres')) return;
  1125. let url = link.href.replace(/^http:\/\//, "https://");
  1126. link.href = url;
  1127. if (link.dataset.resolvedThread) return;
  1128. let hostname;
  1129. try {
  1130. hostname = new URL(url).hostname;
  1131. } catch (e) {
  1132. return;
  1133. }
  1134. if (!hostname.endsWith(".2chan.net") || !url.includes("/res/")) return;
  1135.  
  1136. const idMatch = url.match(/\/res\/(\d+)\.htm$/);
  1137. if (!idMatch) return;
  1138. const threadId = idMatch[1];
  1139.  
  1140. if (resolvedThreadCache[url]) {
  1141. const cache = resolvedThreadCache[url];
  1142. if (cache.type === 'original') {
  1143. link.textContent = `>>${threadId} ${cache.title}」 (別スレ)`;
  1144. } else if (cache.type === 'archive') {
  1145. link.href = cache.archiveUrl;
  1146. link.textContent = `>>${threadId} ${cache.title}」 (過去ログ)`;
  1147. } else {
  1148. link.textContent = `>>${threadId} (別スレ) (過去ログが見つかりません)`;
  1149. }
  1150. link.dataset.resolvedThread = "true";
  1151. return;
  1152. }
  1153.  
  1154. GM_xmlhttpRequest({
  1155. method: "GET",
  1156. url: url + (url.includes('?') ? '&' : '?') + '_=' + Date.now(),
  1157. onload: function(response) {
  1158. const parser = new DOMParser();
  1159. const doc = parser.parseFromString(response.responseText, "text/html");
  1160. const titleEl = doc.querySelector("title");
  1161. let title = titleEl ? titleEl.textContent.trim() : "タイトル取得失敗";
  1162. if (response.status === 200 && !title.includes("404 File Not Found")) {
  1163. resolvedThreadCache[url] = { type: 'original', title: title };
  1164. updateLink(link, threadId, title, url, 'original');
  1165. } else {
  1166. const archiveUrls = generateArchiveUrls(new URL(url).hostname, new URL(url).pathname.split('/')[1], threadId);
  1167. checkArchiveUrls(archiveUrls, 0, function(archiveUrl, archiveTitle) {
  1168. if (archiveUrl) {
  1169. resolvedThreadCache[url] = { type: 'archive', title: archiveTitle, archiveUrl: archiveUrl };
  1170. updateLink(link, threadId, archiveTitle, archiveUrl, 'archive');
  1171. } else {
  1172. resolvedThreadCache[url] = { type: 'not_found' };
  1173. updateLink(link, threadId, null, url, 'not_found');
  1174. }
  1175. });
  1176. }
  1177. },
  1178. onerror: function() {
  1179. resolvedThreadCache[url] = { type: 'not_found' };
  1180. updateLink(link, threadId, null, url, 'not_found');
  1181. }
  1182. });
  1183. });
  1184. }
  1185.  
  1186.  
  1187. function generateArchiveUrls(host, board, threadId) {
  1188. if (host.includes("may.2chan.net") && board === "b") {
  1189. return [
  1190. `https://may1.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`,
  1191. `https://may2.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`,
  1192. `https://may3.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`,
  1193. `https://kako.futakuro.com/futa/may_b/${threadId}/`,
  1194. `https://futabaforest.net/b/res/${threadId}.htm`
  1195. ];
  1196. }
  1197. if (host.includes("img.2chan.net") && board === "b") {
  1198. return [
  1199. `https://c3.ftbucket.info/img/cont/img.2chan.net_b_res_${threadId}/index.htm`,
  1200. `https://kako.futakuro.com/futa/img_b/${threadId}/`
  1201. ];
  1202. }
  1203. if (host.includes("jun.2chan.net") && board === "jun") {
  1204. return [
  1205. `https://c3.ftbucket.info/jun/cont/jun.2chan.net_jun_res_${threadId}/index.htm`
  1206. ];
  1207. }
  1208. if (host.includes("dec.2chan.net")) {
  1209. return [
  1210. `https://c3.ftbucket.info/other/cont/${host.replace(/\./g, '_')}_${board}_res_${threadId}/index.htm`
  1211. ];
  1212. }
  1213. return [];
  1214. }
  1215.  
  1216. function checkArchiveUrls(urls, index, callback) {
  1217. if (index >= urls.length) return callback(null, null);
  1218.  
  1219. let options = {
  1220. method: "HEAD",
  1221. url: urls[index],
  1222. onload: function(res) {
  1223. if (res.status >= 200 && res.status < 400) {
  1224. if (urls[index].includes("ftbucket.info")) {
  1225. GM_xmlhttpRequest({
  1226. method: "GET",
  1227. url: urls[index],
  1228. responseType: "arraybuffer",
  1229. onload: function(getRes) {
  1230. let decoder = new TextDecoder("shift_jis");
  1231. let text = decoder.decode(getRes.response);
  1232. const parser = new DOMParser();
  1233. const doc = parser.parseFromString(text, "text/html");
  1234. const titleEl = doc.querySelector("title");
  1235. let title = titleEl ? titleEl.textContent.trim() : "";
  1236. if (!title || title === "タイトル不明") {
  1237. checkArchiveUrls(urls, index + 1, callback);
  1238. } else {
  1239. callback(urls[index], title);
  1240. }
  1241. },
  1242. onerror: function() {
  1243. checkArchiveUrls(urls, index + 1, callback);
  1244. }
  1245. });
  1246. }
  1247. else if (urls[index].includes("futakuro.com")) {
  1248. GM_xmlhttpRequest({
  1249. method: "GET",
  1250. url: urls[index],
  1251. onload: function(getRes) {
  1252. const parser = new DOMParser();
  1253. const doc = parser.parseFromString(getRes.responseText, "text/html");
  1254. const titleEl = doc.querySelector("title");
  1255. let title = titleEl ? titleEl.textContent.trim() : "";
  1256. if (!title || title === "タイトル不明") {
  1257. checkArchiveUrls(urls, index + 1, callback);
  1258. } else {
  1259. callback(urls[index], title);
  1260. }
  1261. },
  1262. onerror: function() {
  1263. checkArchiveUrls(urls, index + 1, callback);
  1264. }
  1265. });
  1266. }
  1267. else if (urls[index].includes("futabaforest.net")) {
  1268. options.timeout = 10000;
  1269. options.ontimeout = function() {
  1270. checkArchiveUrls(urls, index + 1, callback);
  1271. };
  1272. GM_xmlhttpRequest({
  1273. method: "GET",
  1274. url: urls[index],
  1275. responseType: "text",
  1276. onload: function(getRes) {
  1277. const parser = new DOMParser();
  1278. const doc = parser.parseFromString(getRes.responseText, "text/html");
  1279. const titleEl = doc.querySelector("title");
  1280. let title = titleEl ? titleEl.textContent.trim() : "";
  1281. if (!title || title === "タイトル不明") {
  1282. checkArchiveUrls(urls, index + 1, callback);
  1283. } else {
  1284. callback(urls[index], title);
  1285. }
  1286. },
  1287. onerror: function() {
  1288. checkArchiveUrls(urls, index + 1, callback);
  1289. }
  1290. });
  1291. }
  1292. else {
  1293. GM_xmlhttpRequest({
  1294. method: "GET",
  1295. url: urls[index],
  1296. onload: function(getRes) {
  1297. const parser = new DOMParser();
  1298. const doc = parser.parseFromString(getRes.responseText, "text/html");
  1299. const titleEl = doc.querySelector("title");
  1300. let title = titleEl ? titleEl.textContent.trim() : "";
  1301. if (!title || title === "タイトル不明") {
  1302. checkArchiveUrls(urls, index + 1, callback);
  1303. } else {
  1304. callback(urls[index], title);
  1305. }
  1306. },
  1307. onerror: function() {
  1308. checkArchiveUrls(urls, index + 1, callback);
  1309. }
  1310. });
  1311. }
  1312. } else {
  1313. checkArchiveUrls(urls, index + 1, callback);
  1314. }
  1315. },
  1316. onerror: function() {
  1317. checkArchiveUrls(urls, index + 1, callback);
  1318. }
  1319. };
  1320. GM_xmlhttpRequest(options);
  1321. }
  1322.  
  1323. function updateLink(link, threadId, title, newUrl, type) {
  1324. if (type === 'original') {
  1325. link.textContent = `>>${threadId} ${title}」 (別スレ)`;
  1326. } else if (type === 'archive') {
  1327. link.href = newUrl;
  1328. link.textContent = `>>${threadId} ${title}」 (過去ログ)`;
  1329. } else {
  1330. link.textContent = `>>${threadId} (別スレ) (過去ログが見つかりません)`;
  1331. }
  1332. link.dataset.resolvedThread = "true";
  1333. }
  1334.  
  1335.  
  1336. function processNodes(root = document) {
  1337. removeUnwantedElements(root);
  1338. addDownloadButtons(root);
  1339. updateFileSizes(root);
  1340. attachFullSizeHover();
  1341. changeFaviconIfCondition();
  1342. addYouTubeEmbeds(root);
  1343. cleanseJumpLinks(root);
  1344. insertQuoteBreaks(root);
  1345. root.querySelectorAll("td.rtd").forEach(processPost);
  1346. if (window.location.href.indexOf("/res/") !== -1) {
  1347. resolveOtherThreadLinks(root);
  1348. }
  1349. }
  1350. processNodes();
  1351. const observer = new MutationObserver(mutations => {
  1352. mutations.forEach(mutation => {
  1353. mutation.addedNodes.forEach(node => {
  1354. if (node.nodeType === 1) { processNodes(node); }
  1355. });
  1356. });
  1357. });
  1358. observer.observe(document.body, { childList: true, subtree: true });
  1359. setInterval(autoReloadThread, 5000);
  1360. setInterval(checkSodCondition, 1000);
  1361.  
  1362. if (window.location.href.indexOf("/res/") !== -1) {
  1363. const scrollKey = "2chan_lastScroll_" + window.location.pathname;
  1364. window.addEventListener("load", () => {
  1365. const lastScroll = localStorage.getItem(scrollKey);
  1366. if (lastScroll) { window.scrollTo(0, parseInt(lastScroll, 10)); }
  1367. });
  1368. window.addEventListener("scroll", () => {
  1369. localStorage.setItem(scrollKey, window.pageYOffset);
  1370. });
  1371. }
  1372.  
  1373. if (window.location.hostname === "www.2chan.net" && window.location.pathname === "/index2.html") {
  1374. const boardMapping = {
  1375. "img_b": "二次元裏img",
  1376. "dec_dec": "二次元裏dec",
  1377. "jun_jun": "二次元裏jun",
  1378. "may_b": "二次元裏may",
  1379. "dat_b": "二次元裏dat",
  1380. "dec_58": "転載不可",
  1381. "dec_59": "転載可",
  1382. "dec_img2": "二次元表",
  1383. "may_id": "二次元ID",
  1384. "dat_43": "二次元業界",
  1385. "dat_20": "甘味",
  1386. "dat_21": "ラーメン",
  1387. "dat_23": "スピグラ",
  1388. "dat_l": "壁紙二",
  1389. "may_25": "麻雀",
  1390. "may_26": "うま",
  1391. "may_27": "ねこ",
  1392. "zip_12": "サッカー",
  1393. "zip_14": "自作絵裏",
  1394. "zip_5": "えろげ",
  1395. "zip_1": "野球",
  1396. "zip_11": "自作絵",
  1397. "zip_2": "ろぼ",
  1398. "dec_63": "映画",
  1399. "zip_3": "自作PC",
  1400. "zip_32": "女装",
  1401. "zip_15": "ばら",
  1402. "zip_7": "ゆり",
  1403. "zip_8": "やおい",
  1404. "dec_65": "刀剣乱舞",
  1405. "dec_64": "占い",
  1406. "dec_66": "ファッション",
  1407. "dec_67": "旅行",
  1408. "dec_68": "子育て",
  1409. "may_webm": "webm",
  1410. "dec_71": "そうだね",
  1411. "zip_p": "お絵かき",
  1412. "nov_q": "落書き",
  1413. "zip_z": "しょくぶつ",
  1414. "dat_d": "どうぶつ",
  1415. "dat_e": "のりもの",
  1416. "dat_j": "二輪",
  1417. "nov_37": "自転車",
  1418. "dat_45": "カメラ",
  1419. "dat_48": "家電",
  1420. "dat_r": "鉄道",
  1421. "dat_t": "料理",
  1422. "dat_44": "おもちゃ",
  1423. "dat_v": "模型",
  1424. "nov_y": "模型裏",
  1425. "jun_47": "模型裏裏",
  1426. "dat_w": "虫",
  1427. "dat_49": "アクア",
  1428. "dec_62": "アウトドア",
  1429. "dec_73": "VTuber",
  1430. "dec_84": "ホロライブ",
  1431. "dec_81": "合成音声",
  1432. "dat_x": "3DCG",
  1433. "dec_85": "人工知能",
  1434. "nov_35": "政治",
  1435. "nov_36": "経済",
  1436. "dec_79": "宗教",
  1437. "dat_38": "尹錫悦",
  1438. "dec_80": "岸田文雄",
  1439. "dec_50": "三次実況",
  1440. "cgi_f": "軍",
  1441. "may_39": "軍裏",
  1442. "dec_74": "FGO",
  1443. "dec_75": "アイマス",
  1444. "dec_86": "ZOIDS",
  1445. "dec_78": "ウメハラ総合",
  1446. "jun_31": "ゲーム",
  1447. "nov_28": "ネトゲ",
  1448. "dec_56": "ソシャゲ",
  1449. "dec_60": "艦これ",
  1450. "dec_82": "任天堂",
  1451. "dec_69": "モアイ",
  1452. "dec_61": "ソニー",
  1453. "dat_10": "ネットキャラ",
  1454. "nov_34": "なりきり",
  1455. "cgi_g": "特撮",
  1456. "cgi_i": "flash",
  1457. "may_40": "東方",
  1458. "dec_55": "東方裏",
  1459. "cgi_k": "壁紙",
  1460. "cgi_m": "数学",
  1461. "zip_6": "ニュース表",
  1462. "dec_76": "昭和",
  1463. "dec_77": "平成",
  1464. "dec_53": "発電",
  1465. "dec_52": "自然災害",
  1466. "dec_83": "コロナ",
  1467. "img_9": "雑談",
  1468. "dec_70": "新板提案",
  1469. "cgi_o": "二次元グロ",
  1470. "jun_51": "二次元グロ裏",
  1471. "cgi_u": "落書き裏",
  1472. "jun_oe": "お絵sql",
  1473. "jun_72": "お絵sqlip",
  1474. "www_hinan": "ふたば避難所",
  1475. "jun_junbi": "準備"
  1476. };
  1477.  
  1478. const RSS_URL = 'https://futapo.futakuro.com/ranking/index.rdf';
  1479. function fetchRSS() {
  1480. GM_xmlhttpRequest({
  1481. method: 'GET',
  1482. url: RSS_URL,
  1483. onload: function(response) {
  1484. if (response.status === 200) {
  1485. const parser = new DOMParser();
  1486. const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
  1487. const items = xmlDoc.getElementsByTagName('item');
  1488. let threads = [];
  1489. for (let i = 0; i < Math.min(9, items.length); i++) {
  1490. const item = items[i];
  1491. const titleRaw = item.getElementsByTagName('title')[0].textContent;
  1492. const title = titleRaw.replace(/^\d+位[::]?/, '').trim();
  1493.  
  1494. let link = item.getElementsByTagName('link')[0].textContent;
  1495. if(link.indexOf('//') === 0){
  1496. link = 'https:' + link;
  1497. }
  1498.  
  1499. let match = link.match(/https?:\/\/([^\.]+)\.2chan\.net\/([^\/]+)\/res\//);
  1500. let subdomain = '', identifier = '', boardName = 'unknown';
  1501. if(match) {
  1502. subdomain = match[1];
  1503. identifier = match[2];
  1504. let boardKey = subdomain + "_" + identifier;
  1505. if(boardMapping[boardKey]){
  1506. boardName = boardMapping[boardKey];
  1507. } else {
  1508. boardName = identifier;
  1509. }
  1510. }
  1511.  
  1512. const descCDATA = item.getElementsByTagName('description')[0].textContent;
  1513. let imgSrc = '';
  1514. const imgMatch = descCDATA.match(/<img\s+src=["']([^"']+)["']/);
  1515. if (imgMatch) {
  1516. imgSrc = imgMatch[1];
  1517. }
  1518. threads.push({ subdomain, identifier, boardName, imgSrc, title, link });
  1519. }
  1520. insertGrid(threads);
  1521. }
  1522. }
  1523. });
  1524. }
  1525.  
  1526. function insertGrid(threads) {
  1527. const gridTable = document.createElement('table');
  1528. gridTable.style.margin = '20px auto';
  1529. gridTable.style.borderCollapse = 'collapse';
  1530. gridTable.style.fontFamily = 'sans-serif';
  1531. gridTable.style.fontSize = '10px';
  1532. gridTable.style.width = '70%';
  1533. gridTable.style.maxWidth = '800px';
  1534. gridTable.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
  1535. gridTable.style.border = '1px solid #000000';
  1536. gridTable.style.tableLayout = 'fixed';
  1537.  
  1538. const thead = document.createElement('thead');
  1539. const headerRow = document.createElement('tr');
  1540. const headerCell = document.createElement('th');
  1541. headerCell.colSpan = 3;
  1542. headerCell.textContent = '勢い上昇中のスレ';
  1543. headerCell.style.fontSize = '14px';
  1544. headerCell.style.fontWeight = 'bold';
  1545. headerCell.style.color = '#fff';
  1546. headerCell.style.backgroundColor = '#781225';
  1547. headerCell.style.padding = '2px 5px';
  1548. headerCell.style.textAlign = 'center';
  1549. headerRow.appendChild(headerCell);
  1550. thead.appendChild(headerRow);
  1551. gridTable.appendChild(thead);
  1552.  
  1553. const tbody = document.createElement('tbody');
  1554. for (let row = 0; row < 3; row++) {
  1555. const tr = document.createElement('tr');
  1556. for (let col = 0; col < 3; col++) {
  1557. const td = document.createElement('td');
  1558. td.style.border = '1px solid #000000';
  1559. td.style.padding = '4px';
  1560. td.style.textAlign = 'center';
  1561. td.style.verticalAlign = 'top';
  1562. td.style.width = '33%';
  1563. td.style.boxSizing = 'border-box';
  1564. td.style.height = '200px';
  1565. td.style.overflow = 'hidden';
  1566.  
  1567. const index = row * 3 + col;
  1568. if (threads[index]) {
  1569. const { subdomain, identifier, boardName, imgSrc, title, link } = threads[index];
  1570.  
  1571. const boardLink = document.createElement('a');
  1572. boardLink.href = 'https://' + subdomain + '.2chan.net/' + identifier + '/';
  1573. boardLink.target = '_blank';
  1574. boardLink.textContent = boardName;
  1575. boardLink.style.fontSize = '14px';
  1576. boardLink.style.fontWeight = 'bold';
  1577. boardLink.style.marginBottom = '2px';
  1578. boardLink.style.display = 'block';
  1579. td.appendChild(boardLink);
  1580.  
  1581. const anchor = document.createElement('a');
  1582. anchor.href = link;
  1583. anchor.target = '_blank';
  1584. anchor.style.textDecoration = 'none';
  1585. anchor.style.color = 'inherit';
  1586.  
  1587. if (imgSrc) {
  1588. const imgElem = document.createElement('img');
  1589. imgElem.src = imgSrc;
  1590. imgElem.style.maxWidth = '90%';
  1591. imgElem.style.maxHeight = '100px';
  1592. imgElem.style.height = 'auto';
  1593. imgElem.style.marginBottom = '2px';
  1594. anchor.appendChild(imgElem);
  1595. }
  1596. const titleElem = document.createElement('div');
  1597. titleElem.textContent = title;
  1598. titleElem.style.marginTop = '2px';
  1599. titleElem.style.fontSize = '16px';
  1600. anchor.appendChild(titleElem);
  1601.  
  1602. td.appendChild(anchor);
  1603. }
  1604. tr.appendChild(td);
  1605. }
  1606. tbody.appendChild(tr);
  1607. }
  1608. gridTable.appendChild(tbody);
  1609.  
  1610. const existingTables = document.getElementsByTagName('table');
  1611. if (existingTables.length > 0) {
  1612. const targetTable = existingTables[0];
  1613. const spacer = document.createElement('div');
  1614. spacer.style.height = '20px';
  1615. targetTable.parentNode.insertBefore(spacer, targetTable.nextSibling);
  1616. targetTable.parentNode.insertBefore(gridTable, spacer.nextSibling);
  1617. }
  1618. }
  1619.  
  1620. window.addEventListener('load', fetchRSS, false);
  1621. }
  1622.  
  1623.  
  1624. GM_addStyle(`
  1625. .lds-ring,
  1626. .lds-ring div {
  1627. box-sizing: border-box;
  1628. }
  1629. .lds-ring {
  1630. display: inline-block;
  1631. position: relative;
  1632. width: 14px;
  1633. height: 14px;
  1634. }
  1635. .lds-ring div {
  1636. box-sizing: border-box;
  1637. display: block;
  1638. position: absolute;
  1639. width: 15px;
  1640. height: 15px;
  1641. border: 2px solid currentColor;
  1642. border-radius: 50%;
  1643. animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
  1644. border-color: currentColor transparent transparent transparent;
  1645. }
  1646. .lds-ring div:nth-child(1) {
  1647. animation-delay: -0.45s;
  1648. }
  1649. .lds-ring div:nth-child(2) {
  1650. animation-delay: -0.3s;
  1651. }
  1652. .lds-ring div:nth-child(3) {
  1653. animation-delay: -0.15s;
  1654. }
  1655. @keyframes lds-ring {
  1656. 0% { transform: rotate(0deg); }
  1657. 100% { transform: rotate(360deg); }
  1658. }
  1659. `);
  1660.  
  1661.  
  1662. if (!window.location.href.includes('futaba.php?mode=cat')) return;
  1663.  
  1664. if (window.location.href.includes('mode=catset')) return;
  1665.  
  1666. const urlParams = new URLSearchParams(window.location.search);
  1667. const sortParam = urlParams.get('sort');
  1668.  
  1669. let existingThreads = new Set();
  1670. let fetchedThreads = [];
  1671. let board = '';
  1672. let subdomain = '';
  1673. let useCatVersion = false;
  1674.  
  1675. const catalogTable = document.querySelector('#cattable tbody');
  1676. if (!catalogTable) return;
  1677.  
  1678. function recordExistingThreads() {
  1679. catalogTable.querySelectorAll('td').forEach(td => {
  1680. const link = td.querySelector('a[href*="res/"]');
  1681. if (link) {
  1682. existingThreads.add(link.getAttribute('href'));
  1683. }
  1684. });
  1685. }
  1686. recordExistingThreads();
  1687.  
  1688. let maxColumns = 1;
  1689. const firstRow = catalogTable.querySelector('tr');
  1690. if (firstRow) {
  1691. maxColumns = firstRow.children.length;
  1692. }
  1693.  
  1694. const firstImg = catalogTable.querySelector('td img');
  1695. if (firstImg && firstImg.src.includes('/cat/')) {
  1696. useCatVersion = true;
  1697. }
  1698.  
  1699. const preExistingSmall = catalogTable.querySelector('td small');
  1700. const includeTitle = !!(preExistingSmall && preExistingSmall.textContent.trim().length > 0);
  1701. let maxTitleLength = 100;
  1702. if (preExistingSmall) {
  1703. maxTitleLength = preExistingSmall.textContent.trim().length;
  1704. }
  1705.  
  1706. const searchBox = document.createElement('input');
  1707. searchBox.type = 'text';
  1708. searchBox.placeholder = '絞り込み....';
  1709. searchBox.style.margin = '5px';
  1710. searchBox.style.padding = '3px';
  1711. searchBox.style.fontSize = '90%';
  1712. const targetLink = document.querySelector('a[href*="futaba.php?mode=cat"][href*="sort=9"][href*="guid=on"]');
  1713. if (targetLink) {
  1714. targetLink.parentNode.insertBefore(searchBox, targetLink.nextSibling);
  1715. } else {
  1716. document.body.insertBefore(searchBox, document.body.firstChild);
  1717. }
  1718. searchBox.addEventListener('input', () => {
  1719. const query = searchBox.value.trim().toLowerCase();
  1720. const tds = catalogTable.querySelectorAll('td');
  1721. tds.forEach(td => {
  1722. if (query === '') {
  1723. td.style.display = '';
  1724. } else {
  1725. const small = td.querySelector('small');
  1726. const text = small ? small.textContent.toLowerCase() : '';
  1727. const href = td.querySelector('a') ? td.querySelector('a').getAttribute('href').toLowerCase() : '';
  1728. td.style.display = (text.includes(query) || href.includes(query)) ? '' : 'none';
  1729. }
  1730. });
  1731. });
  1732.  
  1733. const loadingDialog = document.createElement('div');
  1734. loadingDialog.style.position = 'fixed';
  1735. loadingDialog.style.top = '46px';
  1736. loadingDialog.style.right = '20px';
  1737. loadingDialog.style.background = 'rgba(0,0,0,0.7)';
  1738. loadingDialog.style.color = '#fff';
  1739. loadingDialog.style.padding = '10px 15px';
  1740. loadingDialog.style.borderRadius = '5px';
  1741. loadingDialog.style.zIndex = '9999';
  1742. loadingDialog.style.fontSize = '14px';
  1743. loadingDialog.style.display = 'flex';
  1744. loadingDialog.style.alignItems = 'center';
  1745.  
  1746. loadingDialog.innerHTML = `
  1747. <div class="lds-ring"><div></div><div></div><div></div><div></div></div>
  1748. <span id="loadingText" style="margin-left: 10px;">カタログを取得しています...</span>
  1749. `;
  1750. document.body.appendChild(loadingDialog);
  1751.  
  1752. (function extractBoard() {
  1753. const parts = window.location.hostname.split('.');
  1754. subdomain = parts[0];
  1755. const pathParts = window.location.pathname.split('/');
  1756. board = pathParts[1];
  1757. })();
  1758.  
  1759. function isInsideExcludedElement(el) {
  1760. let p = el.parentElement;
  1761. while (p) {
  1762. if (['table', 'tr', 'td'].includes(p.tagName.toLowerCase())) return true;
  1763. p = p.parentElement;
  1764. }
  1765. return false;
  1766. }
  1767.  
  1768. function createNewRow() {
  1769. const tr = document.createElement('tr');
  1770. catalogTable.appendChild(tr);
  1771. return tr;
  1772. }
  1773. let currentRow = catalogTable.lastElementChild || createNewRow();
  1774.  
  1775. function addThreadToDOM(thread) {
  1776. if (existingThreads.has(thread.href)) return;
  1777. existingThreads.add(thread.href);
  1778. let titleHtml = "";
  1779. if (includeTitle && typeof thread.title === 'string' && thread.title.trim().length > 0) {
  1780. let t = thread.title.trim();
  1781. if (t.length > maxTitleLength) {
  1782. t = t.substring(0, maxTitleLength) + '...';
  1783. }
  1784. titleHtml = `<br><small>${t}</small>`;
  1785. }
  1786. const td = document.createElement('td');
  1787. td.innerHTML = `
  1788. <a href="${thread.href}" target="_blank">
  1789. <img src="${thread.imgSrc}" border="0" width="${thread.width}" height="${thread.height}" alt="" loading="lazy">
  1790. </a>
  1791. ${titleHtml}
  1792. <br><font size="2">${thread.replyCount}</font><div class="pdmc" data-no="${thread.id}"></div>
  1793. `;
  1794. if (currentRow.children.length >= maxColumns) {
  1795. currentRow = createNewRow();
  1796. }
  1797. currentRow.appendChild(td);
  1798. }
  1799.  
  1800. function addThreadToCatalog(thread) {
  1801. if (existingThreads.has(thread.href)) return;
  1802. if (sortParam && !['6','7','8','9'].includes(sortParam)) {
  1803. fetchedThreads.push(thread);
  1804. return;
  1805. }
  1806. addThreadToDOM(thread);
  1807. }
  1808. function finalizeSortedThreads() {
  1809. if (!sortParam || ['6','7','8','9'].includes(sortParam)) return;
  1810. if (sortParam === '1') {
  1811. fetchedThreads.sort((a, b) => new Date(b.date) - new Date(a.date));
  1812. fetchedThreads.reverse();
  1813. } else if (sortParam === '2') {
  1814. fetchedThreads.sort((a, b) => new Date(a.date) - new Date(b.date));
  1815. fetchedThreads.reverse();
  1816. } else if (sortParam === '3') {
  1817. fetchedThreads.sort((a, b) => b.replyCount - a.replyCount);
  1818. } else if (sortParam === '4') {
  1819. fetchedThreads.sort((a, b) => a.replyCount - b.replyCount);
  1820. }
  1821. fetchedThreads.forEach(thread => addThreadToDOM(thread));
  1822. }
  1823.  
  1824. function gmFetch(url, attempts = 0) {
  1825. const maxAttempts = 10;
  1826. return new Promise(resolve => {
  1827. function attempt() {
  1828. GM_xmlhttpRequest({
  1829. method: 'GET',
  1830. url: url,
  1831. onload: resp => resolve(resp),
  1832. onerror: err => {
  1833. if (attempts < maxAttempts) {
  1834. attempts++;
  1835. console.warn(`Error fetching ${url}, attempt ${attempts}`, err);
  1836. attempt();
  1837. } else {
  1838. console.error(`Max attempts reached for ${url}. Returning empty`);
  1839. resolve({ status: 408, responseText: "" });
  1840. }
  1841. }
  1842. });
  1843. }
  1844. attempt();
  1845. });
  1846. }
  1847.  
  1848. function fetchThreadPromise(threadURL, relativeHref) {
  1849. return new Promise(resolve => {
  1850. gmFetch(threadURL).then(response => {
  1851. const parser = new DOMParser();
  1852. const doc = parser.parseFromString(response.responseText, "text/html");
  1853. const threadIDMatch = relativeHref.match(/res\/(\d+)\.htm/);
  1854. const threadID = threadIDMatch ? threadIDMatch[1] : "";
  1855.  
  1856. let imgElement = null;
  1857. const imgs = doc.querySelectorAll("img");
  1858. imgs.forEach(img => {
  1859. if (!img.getAttribute("src").includes(`/${board}/thumb/`)) return;
  1860. if (isInsideExcludedElement(img)) return;
  1861. if (!imgElement) imgElement = img;
  1862. });
  1863. if (!imgElement) {
  1864. resolve();
  1865. return;
  1866. }
  1867. const thumbSrc = imgElement.getAttribute("src");
  1868. const catSrc = thumbSrc.replace(`/${board}/thumb/`, `/${board}/cat/`);
  1869. const thumbWidth = imgElement.getAttribute("width") || "50";
  1870. const thumbHeight = imgElement.getAttribute("height") || "50";
  1871. const finalSrc = useCatVersion ? catSrc : thumbSrc;
  1872.  
  1873. function processThread(finalW, finalH) {
  1874. let blockquotes = doc.querySelectorAll("blockquote");
  1875. let threadText = "";
  1876. for (const bq of blockquotes) {
  1877. if (isInsideExcludedElement(bq)) continue;
  1878. threadText = bq.textContent.trim();
  1879. if (threadText) break;
  1880. }
  1881.  
  1882. let dateSpans = doc.querySelectorAll("span.cnw");
  1883. let threadDate = "";
  1884. for (const sp of dateSpans) {
  1885. if (isInsideExcludedElement(sp)) continue;
  1886. threadDate = sp.textContent.trim();
  1887. if (threadDate) break;
  1888. }
  1889.  
  1890. let replyCount = doc.querySelectorAll("table").length;
  1891. let title = threadText.split("\n")[0] || "";
  1892.  
  1893. const threadInfo = {
  1894. href: threadURL,
  1895. id: threadID,
  1896. imgSrc: finalSrc,
  1897. width: finalW,
  1898. height: finalH,
  1899. title: title,
  1900. date: threadDate,
  1901. replyCount: replyCount
  1902. };
  1903. addThreadToCatalog(threadInfo);
  1904. resolve();
  1905. }
  1906.  
  1907. if (useCatVersion) {
  1908. let timedOut = false;
  1909. const tId = setTimeout(() => {
  1910. timedOut = true;
  1911. console.warn(`Image load timeout for ${catSrc}, fallback size`);
  1912. processThread(50, 50);
  1913. }, 500);
  1914. let tempImg = new Image();
  1915. tempImg.onload = () => {
  1916. if (!timedOut) {
  1917. clearTimeout(tId);
  1918. processThread(tempImg.naturalWidth, tempImg.naturalHeight);
  1919. }
  1920. };
  1921. tempImg.onerror = () => {
  1922. if (!timedOut) {
  1923. clearTimeout(tId);
  1924. processThread(50, 50);
  1925. }
  1926. };
  1927. tempImg.src = catSrc;
  1928. } else {
  1929. processThread(thumbWidth, thumbHeight);
  1930. }
  1931. }).catch(() => { resolve(); });
  1932. });
  1933. }
  1934.  
  1935. function fetchIndexPagePromise(pageUrl) {
  1936. console.log("Fetching index page:", pageUrl);
  1937. return new Promise(resolve => {
  1938. gmFetch(pageUrl).then(response => {
  1939. if (response.status === 404 || response.responseText.includes("404 File Not Found")) {
  1940. console.log(`Index page ${pageUrl} not found, skipping.`);
  1941. resolve();
  1942. return;
  1943. }
  1944. const parser = new DOMParser();
  1945. const doc = parser.parseFromString(response.responseText, "text/html");
  1946. let threadPromises = [];
  1947. const links = doc.querySelectorAll("a.hsbn");
  1948. links.forEach(link => {
  1949. const relHref = link.getAttribute("href");
  1950. console.log("Found thread URL:", relHref, "on page:", pageUrl);
  1951. const threadURL = `${window.location.protocol}//${subdomain}.2chan.net/${board}/` + relHref;
  1952. if (!existingThreads.has(relHref)) {
  1953. existingThreads.add(relHref);
  1954. threadPromises.push(fetchThreadPromise(threadURL, relHref));
  1955. }
  1956. });
  1957. Promise.all(threadPromises).then(resolve).catch(resolve);
  1958. }).catch(resolve);
  1959. });
  1960. }
  1961.  
  1962. const baseURL = `${window.location.protocol}//${subdomain}.2chan.net/${board}/`;
  1963. const indexPages = [];
  1964. indexPages.push(baseURL + "futaba.htm");
  1965. for (let i = 1; i <= 20; i++) {
  1966. indexPages.push(baseURL + i + ".htm");
  1967. }
  1968.  
  1969. if (['6','7','8','9'].includes(sortParam)) {
  1970. loadingDialog.style.display = "none";
  1971.  
  1972. setTimeout(() => {
  1973. loadingDialog.style.display = "none";
  1974. }, 1500);
  1975. return;
  1976. }
  1977.  
  1978. const indexPromises = indexPages.map(url => fetchIndexPagePromise(url));
  1979.  
  1980. Promise.all(indexPromises).then(() => {
  1981. if (sortParam) {
  1982. finalizeSortedThreads();
  1983. }
  1984. loadingDialog.innerHTML = `
  1985. <div style="font-size: 14px; margin-right: 6px;">✅</div>
  1986. <div id="loadingText" style="color: #fff; font-weight: bold;">カタログを取得しました!</div>
  1987. `;
  1988. loadingDialog.style.background = "green";
  1989. loadingDialog.style.opacity = "1";
  1990. setTimeout(() => {
  1991. loadingDialog.style.display = "none";
  1992. }, 1500);
  1993. });
  1994. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址