您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自動更新、引用元に引用アンカー追加、マウスオーバーで画像拡大表示などお役立ち機能を追加するスクリプト
// ==UserScript== // @name FutabaX // @namespace http://tampermonkey.net/ // @version 1.1 // @description 自動更新、引用元に引用アンカー追加、マウスオーバーで画像拡大表示などお役立ち機能を追加するスクリプト // @author としあき // @match *://*.2chan.net/* // @match https://www.2chan.net/index2.html // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect 2chan.net // @connect futakuro.com // @connect futabaforest.net // @connect ftbucket.info // @license MIT // ==/UserScript== (function() { 'use strict'; GM_addStyle(` td.rtd { white-space: normal !important; word-wrap: break-word !important; overflow-wrap: break-word !important; text-align: left !important; } a[data-quote-post] { display: inline-block !important; white-space: nowrap; margin: 2px 5px 2px 0 !important; } `); let resolvedThreadCache = {}; if (window.top === window.self && document.getElementsByTagName("frameset").length > 0) { window.location.replace("https://www.2chan.net/index2.html"); } if (window.location.href.indexOf("index2.html") !== -1) { function modifyFrontPageLinks() { document.querySelectorAll('a[href="https://dec.2chan.net/dec/futaba.htm"]').forEach(a => { a.textContent = "二次元裏dec"; }); document.querySelectorAll('a[href="https://jun.2chan.net/jun/futaba.htm"]').forEach(a => { a.textContent = "二次元裏jun"; }); document.querySelectorAll('a[href="https://may.2chan.net/b/futaba.htm"]').forEach(a => { a.textContent = "二次元裏may"; }); const mayLink = document.querySelector('a[href="https://may.2chan.net/b/futaba.htm"]'); if (mayLink) { mayLink.insertAdjacentHTML("afterend", '<br><a href="https://img.2chan.net/b/futaba.htm">二次元裏img</a><br>' + '<a href="https://cgi.2chan.net/b/futaba.htm">二次元裏cgi</a><br>' + '<a href="https://dat.2chan.net/b/futaba.htm">二次元裏dat</a>' ); } document.querySelectorAll('a[href="https://may.2chan.net/b/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); document.querySelectorAll('a[href="https://img.2chan.net/b/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); document.querySelectorAll('a[href="https://dec.2chan.net/84/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); document.querySelectorAll('a[href="https://dec.2chan.net/60/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); document.querySelectorAll('a[href="https://dec.2chan.net/55/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); document.querySelectorAll('a[href="https://dec.2chan.net/73/futaba.htm"]').forEach(a => { a.insertAdjacentHTML("afterend", '<span style="color:red;font-size:80%">人気</span>'); }); } modifyFrontPageLinks(); } const originalFavicon = "https://2chan.net/favicon.ico"; 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"; const newReplyFaviconBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRF////R7RHLZoti/iLY9Bj/wAAERW9LQAAAFFJREFUeJxjZIACRiwMIca3DMz8/xgZnc/qHzB8rMvIxG984b2g416g1H+Dg/Yiu4GKhQQUHhivBuliVrgbCmYwKDzAxWDSuwAVQbILLhK6GgDWTBmEbxfYVgAAAABJRU5ErkJggg=="; let thresholdMode = false; const extraStyle = document.createElement("style"); extraStyle.innerHTML = ` .highlighted { background-color: #EDD0BD !important; transition: background-color 2s ease-out; } .quote-preview { background-color: #F0E0D6; padding: 8px; border: 1px solid #F0E0D6; max-width: 90vw; overflow: auto; box-shadow: 2px 2px 10px rgba(0,0,0,0.5); } #replyContainer { position: fixed; bottom: 20px; right: 20px; background: #fff; border: 1px solid #ccc; padding: 5px; z-index: 15000; box-shadow: 2px 2px 10px rgba(0,0,0,0.5); } #replyDragHandle { background: #ccc; padding: 4px; cursor: move; font-size: 0.9em; text-align: center; user-select: none; } `; document.head.appendChild(extraStyle); function highlightPost(post) { post.classList.add("highlighted"); setTimeout(() => { post.classList.remove("highlighted"); }, 2000); } let currentQuotePreview = null; function handleQuoteLinkMouseEnter(e) { const link = this; const quoteNum = link.dataset.quotePost; const target = document.getElementById("post-" + quoteNum); if (!target) return; currentQuotePreview = target.cloneNode(true); currentQuotePreview.classList.add("quote-preview"); currentQuotePreview.style.position = "fixed"; currentQuotePreview.style.zIndex = "20000"; currentQuotePreview.style.left = (e.clientX + 10) + "px"; currentQuotePreview.style.top = (e.clientY - 10) + "px"; document.body.appendChild(currentQuotePreview); } function handleQuoteLinkMouseMove(e) { if (currentQuotePreview) { currentQuotePreview.style.left = (e.clientX + 10) + "px"; currentQuotePreview.style.top = (e.clientY - 10) + "px"; } } function handleQuoteLinkMouseLeave(e) { if (currentQuotePreview) { currentQuotePreview.remove(); currentQuotePreview = null; } } function removeUnwantedElements(root = document) { root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/sphead.htm"], iframe[src^="https://dec.2chan.net/bin/spfoot_a.htm"]').forEach(iframe => { if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('div').forEach(div => { if (div.innerHTML.includes("ヘッダ広告ここから")) { div.remove(); } }); root.querySelectorAll('.tue2').forEach(el => el.remove()); root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/overlay.htm"]').forEach(iframe => { if (iframe.parentElement) { let parent = iframe.parentElement; parent.remove(); let prev = parent.previousElementSibling; if (prev && prev.tagName === 'DIV' && prev.getAttribute('style') && prev.getAttribute('style').includes('height:68px')) { prev.remove(); } } }); root.querySelectorAll('style').forEach(styleEl => { if (styleEl.innerHTML.includes('.footfix')) { styleEl.remove(); } }); root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/foot2_a.htm"]').forEach(iframe => { if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/hsi1.htm"], iframe[src^="https://dec.2chan.net/bin/foot4_ab.htm"]').forEach(iframe => { if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/hsif.htm"]').forEach(iframe => { if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('iframe[src^="/bin/catp.htm"]').forEach(iframe => { let container = iframe.closest('div[style*="width:610px"]'); if (container) { container.remove(); } else if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('.footfix').forEach(el => el.remove()); ["imobile_adspotdiv1", "imobile_adspotdiv2"].forEach(id => { const el = document.getElementById(id); if (el) { el.style.display = "none"; } }); root.querySelectorAll('div[class^="adWrapper"]').forEach(el => el.remove()); root.querySelectorAll('#rightadc').forEach(el => el.remove()); root.querySelectorAll('div#rightadfloat').forEach(el => el.remove()); root.querySelectorAll('div.heaven-728-90.ninja-slider.mode-b.dark').forEach(el => el.remove()); root.querySelectorAll('iframe[src^="https://dec.2chan.net/bin/foot1_n.htm"]').forEach(iframe => { if (iframe.parentElement) { iframe.parentElement.remove(); } }); root.querySelectorAll('div[id*="ad" i]').forEach(el => el.remove()); root.querySelectorAll('div[style*="width:680px"][style*="margin: 0 auto"]').forEach(el => el.remove()); } function cleanseJumpLinks(root = document) { const jumpLinks = root.querySelectorAll('a[href^="/bin/jump.php?"]'); jumpLinks.forEach(link => { let href = link.getAttribute('href'); let cleaned = href.replace(/^\/bin\/jump\.php\?/, ''); link.setAttribute('href', cleaned); }); } function isSrcLink(anchor) { return /\.(png|jpe?g|gif|mp4|webm)$/i.test(anchor.href); } function addDownloadButtons(root = document) { const allAnchors = Array.from( root.querySelectorAll('a[href*="/src/"], a[href*="fu"]') ).filter(isSrcLink); const filenameAnchors = allAnchors.filter(anchor => !anchor.querySelector('img')); filenameAnchors.forEach(anchor => { if (anchor.nextElementSibling && anchor.nextElementSibling.classList?.contains('download-btn')) { return; } const downloadLink = document.createElement('a'); downloadLink.href = anchor.href; downloadLink.download = ''; downloadLink.className = 'download-btn'; downloadLink.style.display = 'inline'; downloadLink.style.verticalAlign = 'middle'; downloadLink.style.marginLeft = '5px'; if (/\.(mp4|webm)$/i.test(downloadLink.href)) { downloadLink.addEventListener('click', function(e) { e.preventDefault(); fetch(downloadLink.href) .then(resp => resp.blob()) .then(blob => { const blobUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = downloadLink.href.split('/').pop(); document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(blobUrl); }) .catch(err => console.error("Download failed", err)); }); } const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("xmlns", svgNS); svg.setAttribute("viewBox", "0 0 512 512"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); svg.style.width = "1em"; svg.style.height = "1em"; svg.style.verticalAlign = "middle"; svg.style.fill = "currentColor"; const path = document.createElementNS(svgNS, "path"); 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"); svg.appendChild(path); downloadLink.appendChild(svg); anchor.insertAdjacentElement('afterend', downloadLink); }); } function processPost(td) { const bq = td.querySelector("blockquote"); if (!bq) return; const originalHTML = bq.innerHTML; const lines = originalHTML.split(/<br\s*\/?>/i); const imageFiles = new Set(); const videoFiles = new Set(); lines.forEach(line => { const div = document.createElement("div"); div.innerHTML = line; const text = div.textContent.trim(); if (text.startsWith(">") || text.startsWith(">")) return; const matches = text.match(/\b(fu?\d+\.\w+)\b/gi); if (!matches) return; matches.forEach(fullname => { const ext = (fullname.split(".").pop() || "").toLowerCase(); if (ext === "mp4" || ext === "webm") { videoFiles.add(fullname); } else { imageFiles.add(fullname); } }); }); if (imageFiles.size > 0) { td.querySelectorAll("a[href*='/up/src/'], a[href*='/up2/src/']").forEach(a => { if (!bq.contains(a)) { a.remove(); } }); const container = document.createElement("div"); container.style.margin = "0"; container.style.padding = "0"; container.style.display = "block"; Array.from(imageFiles).forEach((filename, index, arr) => { const baseUrl = filename.toLowerCase().startsWith("fu") ? "https://dec.2chan.net/up2/src/" : "https://dec.2chan.net/up/src/"; const imageUrl = baseUrl + filename; const thumbLink = document.createElement("a"); thumbLink.href = imageUrl; thumbLink.target = "_blank"; const img = document.createElement("img"); img.src = imageUrl; img.setAttribute("border", "0"); img.setAttribute("align", "left"); img.setAttribute("hspace", "20"); img.setAttribute("alt", "外部画像"); img.setAttribute("loading", "lazy"); img.style.maxWidth = "200px"; img.style.maxHeight = "200px"; thumbLink.appendChild(img); container.appendChild(thumbLink); if (index < arr.length - 1) { container.appendChild(document.createElement("br")); } }); td.insertBefore(container, bq); bq.style.marginLeft = "241px"; } let replacedHTML = originalHTML; replacedHTML = replacedHTML.replace( /\b(fu?\d+\.(mp4|webm))\b/gi, (match, filename) => { const baseUrl = filename.toLowerCase().startsWith("fu") ? "https://dec.2chan.net/up2/src/" : "https://dec.2chan.net/up/src/"; const videoUrl = baseUrl + filename; return `<a href="${videoUrl}" target="_blank">${filename}</a>` + ` <span class="embed-video-link" data-url="${videoUrl}" style="cursor:pointer;">(動画)</span>`; } ); replacedHTML = replacedHTML.replace( /\b(fu?\d+\.(?!mp4|webm)\w+)\b/gi, (match, filename) => { const baseUrl = filename.toLowerCase().startsWith("fu") ? "https://dec.2chan.net/up2/src/" : "https://dec.2chan.net/up/src/"; const imageUrl = baseUrl + filename; return `<a href="${imageUrl}" target="_blank">${filename}</a>`; } ); bq.innerHTML = replacedHTML; bq.querySelectorAll(".embed-video-link").forEach(el => { el.addEventListener("click", function() { if (this.nextSibling && this.nextSibling.className === "video-iframe-wrap") { this.nextSibling.remove(); return; } const iframeWrap = document.createElement("div"); iframeWrap.className = "video-iframe-wrap"; iframeWrap.style.position = "relative"; iframeWrap.style.marginTop = "5px"; const closeBtn = document.createElement("div"); closeBtn.textContent = "✖"; closeBtn.style.position = "absolute"; closeBtn.style.top = "3px"; closeBtn.style.right = "5px"; closeBtn.style.cursor = "pointer"; closeBtn.style.backgroundColor = "rgba(0,0,0,0.6)"; closeBtn.style.color = "#fff"; closeBtn.style.padding = "2px 6px"; closeBtn.style.borderRadius = "3px"; closeBtn.style.zIndex = "10"; closeBtn.onclick = () => iframeWrap.remove(); const iframe = document.createElement("iframe"); iframe.src = this.dataset.url; iframe.style.width = "400px"; iframe.style.height = "300px"; iframe.style.border = "1px solid #ccc"; iframe.style.borderRadius = "3px"; iframeWrap.appendChild(closeBtn); iframeWrap.appendChild(iframe); this.parentNode.insertBefore(iframeWrap, this.nextSibling); }); }); } function updateFileSizes(root = document) { const elements = root.querySelectorAll('.thre, .rtd'); elements.forEach(el => { el.innerHTML = el.innerHTML.replace(/(-\()(\d+)\s*B\)/g, (match, prefix, num) => { let kb = (parseInt(num, 10) / 1024).toFixed(1); return prefix + kb + " KB)"; }); }); } function attachFullSizeHover(root = document) { const allAnchors = Array.from(root.querySelectorAll('a[href*="/src/"], a[href*="2chan.net/up2/src/"]')).filter(anchor => { return /\.(jpg|jpeg|png|gif)$/i.test(anchor.href); }); const thumbnails = allAnchors .filter(anchor => anchor.querySelector('img')) .map(anchor => anchor.querySelector('img')); thumbnails.forEach(img => { if (img.dataset.fullsizeHoverAttached) return; img.addEventListener('mouseenter', e => { showFullSizePreview(e.currentTarget); }); img.addEventListener('mouseleave', e => { hideFullSizePreview(); }); img.dataset.fullsizeHoverAttached = "true"; }); } function showFullSizePreview(img) { hideFullSizePreview(); const fullSrc = img.parentElement.href; const preview = document.createElement('img'); preview.id = 'fullsize-preview'; preview.src = fullSrc; preview.style.position = 'fixed'; preview.style.top = '50%'; preview.style.left = '50%'; preview.style.transform = 'translate(-50%, -50%)'; preview.style.maxWidth = '90vw'; preview.style.maxHeight = '90vh'; preview.style.zIndex = '2147483647'; preview.style.border = '1px solid #ccc'; preview.style.pointerEvents = 'none'; document.body.appendChild(preview); } function hideFullSizePreview() { const preview = document.getElementById('fullsize-preview'); if (preview) preview.remove(); } function autoReloadThread() { if (document.title.includes("404 File Not Found")) return; const reloadAnchor = document.querySelector('#contres a'); if (reloadAnchor) { const onClickAttr = reloadAnchor.getAttribute('onClick'); const match = onClickAttr && onClickAttr.match(/scrlf\((\d+)\)/); if (match) { const threadId = match[1]; if (typeof unsafeWindow.scrlf === 'function') { unsafeWindow.scrlf(threadId); } } } } setInterval(autoReloadThread, 5000); function changeFavicon(newIconData) { let link = document.querySelector("link[rel*='icon']"); if (!link) { link = document.createElement("link"); link.rel = "icon"; document.head.appendChild(link); } link.href = newIconData; } function changeFaviconIfCondition() { if (thresholdMode) return; const maxresElement = document.querySelector('span.maxres'); const contdispElement = document.querySelector('#contdisp'); if ((maxresElement && maxresElement.textContent.includes("上限1000レスに達しました")) || (contdispElement && contdispElement.textContent.includes("スレッドがありません"))) { changeFavicon(thresholdFaviconBase64); thresholdMode = true; } } setInterval(changeFaviconIfCondition, 1000); function checkSodCondition() { if (document.title.includes("404 File Not Found")) return; const sodElements = document.querySelectorAll('.sod'); sodElements.forEach(el => { if (el.textContent.trim() === "そうだねx0" && !el.dataset.sodAlerted) { alert("お使いのipアドレスからそうだねできません"); el.dataset.sodAlerted = "true"; } }); } setInterval(checkSodCondition, 1000); function addQuoteLinks() { if (document.title.includes("404 File Not Found")) return; const allPosts = Array.from(document.querySelectorAll('.thre, .rtd')) .filter(post => post.querySelector('span.cno')); let postDict = {}; allPosts.forEach(post => { const cnoEl = post.querySelector('span.cno'); if (cnoEl) { const num = cnoEl.textContent.trim().replace(/^No\./, ''); if (!post.id) { post.id = "post-" + num; } post.dataset.postNumber = num; const isOP = !post.closest('table') && post.textContent.includes('画像ファイル名'); if (isOP) { post.dataset.isOP = "true"; } postDict[num] = post; } }); const replyPosts = allPosts.filter(post => post.closest("table") !== null); let quotes = []; replyPosts.forEach(post => { const quotePostNum = post.dataset.postNumber; const fonts = Array.from(post.querySelectorAll('blockquote font[color="#789922"]')); fonts.forEach(font => { let text = font.textContent.trim(); text = text.replace(/^>+/, '').replace(/^(>)+/, '').trim(); if (text.indexOf('\n') !== -1) { text = text.split('\n')[0].trim(); } if (!text) return; let type = "text"; let value = text; const numMatch = text.match(/^(?:No\.?\s*)?(\d{8,})$/); if (numMatch) { type = "number"; value = numMatch[1]; } else if (text.match(/\.(jpg|jpeg|png|gif|webp|mp4|webm)$/i)) { type = "filename"; value = text; } quotes.push({ quotePostNum, type, value, sourcePost: post }); }); }); quotes.sort((a, b) => parseInt(a.quotePostNum) - parseInt(b.quotePostNum)); allPosts.forEach(originalPost => { const origPostNum = originalPost.dataset.postNumber; const isOP = originalPost.dataset.isOP === "true"; let lines = []; let anchors = []; let originalFilename = ''; if (isOP) { const filenameAnchor = originalPost.querySelector('a[href*="/src/"]'); if (filenameAnchor) { originalFilename = filenameAnchor.textContent.trim(); } const block = originalPost.querySelector('blockquote'); if (block) { lines = block.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0); } } else { const block = originalPost.querySelector('blockquote'); if (block) { lines = block.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0); } else { lines = originalPost.innerText.split('\n').map(l => l.trim()).filter(l => l.length > 0); } anchors = Array.from(originalPost.querySelectorAll('a[href*="/src/"]')); } let insertAfterElement; if (isOP) { insertAfterElement = originalPost.querySelector('.cntd'); } else { insertAfterElement = originalPost.querySelector('.sod'); } if (!insertAfterElement) return; for (let i = quotes.length - 1; i >= 0; i--) { let quote = quotes[i]; if (quote.sourcePost.id === originalPost.id) continue; let matchFound = false; if (quote.type === "number") { matchFound = (origPostNum === quote.value); } else if (quote.type === "filename") { if (isOP) { matchFound = (originalFilename === quote.value); } else { matchFound = anchors.some(a => a.getAttribute('href').includes(quote.value) || a.textContent.trim() === quote.value ); } } else { matchFound = lines.includes(quote.value); } if (matchFound) { if (!originalPost.querySelector(`a[data-quote-post="${quote.quotePostNum}"]`)) { const link = document.createElement('a'); link.href = "javascript:void(0);"; link.textContent = ">>" + quote.quotePostNum; link.style.marginLeft = "5px"; link.dataset.quotePost = quote.quotePostNum; link.addEventListener('click', () => { const target = document.getElementById("post-" + quote.quotePostNum); if (target) { target.scrollIntoView({ behavior: "smooth" }); highlightPost(target); } }); link.addEventListener('mouseenter', handleQuoteLinkMouseEnter); link.addEventListener('mousemove', handleQuoteLinkMouseMove); link.addEventListener('mouseleave', handleQuoteLinkMouseLeave); insertAfterElement.parentNode.insertBefore(link, insertAfterElement.nextSibling); } } } }); } setInterval(addQuoteLinks, 1000); function insertQuoteBreaks() { var posts = document.querySelectorAll('td.rtd'); posts.forEach(function(post) { var quotes = post.querySelectorAll('a[data-quote-post]'); quotes.forEach(function(quote, index) { if ((index + 1) % 10 === 0) { if (!quote.nextSibling || quote.nextSibling.nodeName !== 'BR') { quote.insertAdjacentHTML('afterend', '<br>'); } } }); }); } function makeDraggable(element, handle) { handle = handle || element; handle.style.cursor = "move"; let offsetX, offsetY; handle.addEventListener("mousedown", function(e) { offsetX = e.clientX - element.getBoundingClientRect().left; offsetY = e.clientY - element.getBoundingClientRect().top; document.addEventListener("mousemove", moveHandler); document.addEventListener("mouseup", upHandler); }); function moveHandler(e) { element.style.left = (e.clientX - offsetX) + "px"; element.style.top = (e.clientY - offsetY) + "px"; element.style.bottom = "auto"; element.style.right = "auto"; } function upHandler(e) { document.removeEventListener("mousemove", moveHandler); document.removeEventListener("mouseup", upHandler); } } function setupReplyForm() { const replyForm = document.getElementById("fm"); if (replyForm) { const reszbElements = replyForm.querySelectorAll("span#reszb"); reszbElements.forEach(el => el.remove()); const ftb2 = replyForm.querySelector("table.ftb2"); if (ftb2) { ftb2.parentNode.removeChild(ftb2); const hrElement = document.querySelector("hr"); if (hrElement) { hrElement.insertAdjacentElement("afterend", ftb2); } else { document.body.insertBefore(ftb2, document.body.firstChild); } } const container = document.createElement("div"); container.id = "replyContainer"; container.style.position = "fixed"; container.style.bottom = "20px"; container.style.right = "20px"; container.style.background = "#fff"; container.style.border = "1px solid #ccc"; container.style.padding = "5px"; container.style.zIndex = "15000"; container.style.boxShadow = "2px 2px 10px rgba(0,0,0,0.5)"; const handle = document.createElement("div"); handle.id = "replyDragHandle"; handle.textContent = "返信フォーム(ドラッグで移動)"; handle.style.background = "#ccc"; handle.style.padding = "4px"; handle.style.cursor = "move"; handle.style.fontSize = "0.9em"; handle.style.textAlign = "center"; handle.style.userSelect = "none"; container.appendChild(handle); container.appendChild(replyForm); document.body.appendChild(container); makeDraggable(container, handle); } } if (window.location.href.indexOf("/res/") !== -1) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", setupReplyForm); } else { setupReplyForm(); } } function addArchiveLinksTo404() { if (!document.title.includes("404 File Not Found")) return; let h1s = document.querySelectorAll("h1"); let targetH1 = null; h1s.forEach(h => { if (h.textContent.includes("掲示板に戻る")) { targetH1 = h; } }); if (!targetH1) return; let hr = targetH1.nextElementSibling; while (hr && hr.tagName !== "HR") { hr = hr.nextElementSibling; } if (!hr) return; let archiveHeader = document.createElement("h1"); archiveHeader.textContent = "過去ログを取得"; hr.insertAdjacentElement("afterend", archiveHeader); let linkContainer = document.createElement("div"); linkContainer.style.marginTop = "10px"; let path = window.location.pathname; let regex = /\/([^\/]+)\/res\/(\d+)\.htm/i; let match = path.match(regex); if (!match) return; let board = match[1]; let threadId = match[2]; let host = window.location.host; let links = []; if (host.includes("may.2chan.net") && board === "b") { links.push({ text: "futakuro", url: `https://kako.futakuro.com/futa/may_b/${threadId}/` }); links.push({ text: "ftbucket (may1)", url: `https://may1.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` }); links.push({ text: "ftbucket (may2)", url: `https://may2.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` }); links.push({ text: "ftbucket (may3)", url: `https://may3.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm` }); links.push({ text: "futabaforest", url: `https://futabaforest.net/b/res/${threadId}.htm` }); } else if (host.includes("img.2chan.net") && board === "b") { links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/img/cont/img.2chan.net_b_res_${threadId}/index.htm` }); links.push({ text: "futakuro", url: `https://kako.futakuro.com/futa/img_b/${threadId}/` }); } else if (host.includes("jun.2chan.net") && board === "jun") { links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/jun/cont/jun.2chan.net_jun_res_${threadId}/index.htm` }); } else if (host.includes("dec.2chan.net") && board === "55") { links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/dec55/cont/dec.2chan.net_55_res_${threadId}/index.htm` }); } else if (host.includes("dec.2chan.net") && board === "60") { links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/dec60/cont/dec.2chan.net_60_res_${threadId}/index.htm` }); } else { links.push({ text: "ftbucket (c3)", url: `https://c3.ftbucket.info/other/cont/${host}_${board}_res_${threadId}/index.htm` }); } links.forEach(linkInfo => { let a = document.createElement("a"); a.href = linkInfo.url; a.textContent = linkInfo.text; a.style.marginRight = "10px"; a.target = "_blank"; linkContainer.appendChild(a); }); archiveHeader.insertAdjacentElement("afterend", linkContainer); } addArchiveLinksTo404(); const originalTitle = document.title; let maxSeenIndex = -1; let prevUnread = 0; let initialUnread = 0; let initialUnreadSet = false; let wasZeroOnBlur = false; function isInViewport(el) { const rect = el.getBoundingClientRect(); return rect.bottom > 0 && rect.top < window.innerHeight; } function updateUnreadCountNew() { if (window.location.href.indexOf("/res/") === -1) return; if (document.title.includes("404 File Not Found")) { changeFavicon(thresholdFaviconBase64); return; } const contdispElement = document.querySelector('#contdisp'); if (contdispElement && contdispElement.textContent.includes("スレッドがありません")) { document.title = `【落ち】 ${originalTitle}`; return; } const replies = Array.from(document.querySelectorAll(".thre, .rtd")) .filter(post => post.closest("table") !== null); const totalReplies = replies.length; let visibleIndex = -1; for (let i = 0; i < replies.length; i++) { if (isInViewport(replies[i])) { visibleIndex = i; } } if (document.hasFocus()) { if (visibleIndex > maxSeenIndex) { maxSeenIndex = visibleIndex; } if (!initialUnreadSet) { initialUnread = totalReplies - (maxSeenIndex + 1); initialUnreadSet = true; } } else { if (!initialUnreadSet) { maxSeenIndex = totalReplies - 1; initialUnread = 0; initialUnreadSet = true; } } const unread = totalReplies - (maxSeenIndex + 1); document.title = `(${unread}) ${originalTitle}`; if (!thresholdMode && !document.hasFocus() && wasZeroOnBlur && prevUnread === 0 && unread > 0) { changeFavicon(newReplyFaviconBase64); } if (document.hasFocus() && unread === 0 && !thresholdMode) { changeFavicon(originalFavicon); } prevUnread = unread; } window.addEventListener("scroll", updateUnreadCountNew); setInterval(updateUnreadCountNew, 1000); window.addEventListener("blur", () => { if (prevUnread === 0) { wasZeroOnBlur = true; } }); window.addEventListener("focus", () => { wasZeroOnBlur = false; if (prevUnread === 0 && !thresholdMode) { changeFavicon(originalFavicon); } }); function addYouTubeEmbeds(root = document) { const ytRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([\w-]{11})/; const processedLinks = new Set(); function createFloatingWindow(videoId) { const floatDiv = document.createElement('div'); floatDiv.style.cssText = ` position: fixed; width: 480px; height: 270px; z-index: 9999; background: white; box-shadow: 0 0 10px rgba(0,0,0,0.5); overflow: hidden; cursor: move; `; const handle = document.createElement('div'); handle.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; height: 20px; background: #eee0d7; cursor: move; user-select: none; `; const closeButton = document.createElement('button'); closeButton.textContent = 'X'; closeButton.style.cssText = ` position: absolute; top: 0; right: 0; padding: 2px 6px; background: #781208; color: white; border: none; cursor: pointer; z-index: 10000; `; closeButton.addEventListener('click', () => floatDiv.remove()); const iframe = document.createElement('iframe'); iframe.style.cssText = ` width: 100%; height: calc(100% - 20px); border: none; margin-top: 20px; `; iframe.src = `https://www.youtube.com/embed/${videoId}?rel=0&wmode=opaque`; iframe.setAttribute('allow', 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'); iframe.setAttribute('allowfullscreen', ''); handle.appendChild(closeButton); floatDiv.appendChild(handle); floatDiv.appendChild(iframe); const leftHandle = document.createElement('div'); leftHandle.style.cssText = ` position: absolute; top: 20px; left: 0; width: 8px; height: calc(100% - 20px); cursor: ew-resize; z-index: 10001; `; const rightHandle = document.createElement('div'); rightHandle.style.cssText = ` position: absolute; top: 20px; right: 0; width: 8px; height: calc(100% - 20px); cursor: ew-resize; z-index: 10001; `; const bottomHandle = document.createElement('div'); bottomHandle.style.cssText = ` position: absolute; bottom: 0; left: 0; height: 8px; width: 100%; cursor: ns-resize; z-index: 10001; `; floatDiv.appendChild(leftHandle); floatDiv.appendChild(rightHandle); floatDiv.appendChild(bottomHandle); document.body.appendChild(floatDiv); const startX = (window.innerWidth - 480) / 2; const startY = (window.innerHeight - 270) / 2; floatDiv.style.left = `${startX}px`; floatDiv.style.top = `${startY}px`; let isDragging = false; let startXPos = 0; let startYPos = 0; let initialMouseX = 0; let initialMouseY = 0; handle.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); function startDrag(e) { isDragging = true; initialMouseX = e.clientX; initialMouseY = e.clientY; startXPos = floatDiv.offsetLeft; startYPos = floatDiv.offsetTop; floatDiv.style.cursor = 'grabbing'; } function drag(e) { if (isDragging) { const deltaX = e.clientX - initialMouseX; const deltaY = e.clientY - initialMouseY; floatDiv.style.left = `${startXPos + deltaX}px`; floatDiv.style.top = `${startYPos + deltaY}px`; } } function stopDrag() { isDragging = false; floatDiv.style.cursor = 'move'; } leftHandle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); let startX = e.clientX; let startLeft = floatDiv.offsetLeft; let startWidth = floatDiv.offsetWidth; function doResize(ev) { let deltaX = ev.clientX - startX; let newWidth = startWidth - deltaX; let newLeft = startLeft + deltaX; if(newWidth < 200) { newWidth = 200; newLeft = startLeft + (startWidth - 200); } floatDiv.style.width = newWidth + 'px'; floatDiv.style.left = newLeft + 'px'; } function stopResize() { document.removeEventListener('mousemove', doResize); document.removeEventListener('mouseup', stopResize); } document.addEventListener('mousemove', doResize); document.addEventListener('mouseup', stopResize); }); rightHandle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); let startX = e.clientX; let startWidth = floatDiv.offsetWidth; function doResize(ev) { let deltaX = ev.clientX - startX; let newWidth = startWidth + deltaX; if(newWidth < 200) { newWidth = 200; } floatDiv.style.width = newWidth + 'px'; } function stopResize() { document.removeEventListener('mousemove', doResize); document.removeEventListener('mouseup', stopResize); } document.addEventListener('mousemove', doResize); document.addEventListener('mouseup', stopResize); }); bottomHandle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); let startY = e.clientY; let startHeight = floatDiv.offsetHeight; function doResize(ev) { let deltaY = ev.clientY - startY; let newHeight = startHeight + deltaY; if(newHeight < 150) { newHeight = 150; } floatDiv.style.height = newHeight + 'px'; } function stopResize() { document.removeEventListener('mousemove', doResize); document.removeEventListener('mouseup', stopResize); } document.addEventListener('mousemove', doResize); document.addEventListener('mouseup', stopResize); }); } function processLink(link) { if (processedLinks.has(link)) return; const match = link.href.match(ytRegex); if (!match) return; const videoId = match[1]; const containerId = `yt-${videoId}-${Date.now()}`; const toggle = document.createElement('a'); toggle.href = "javascript:void(0);"; toggle.textContent = "(埋め込み)"; toggle.style.marginLeft = "5px"; const floatButton = document.createElement('a'); floatButton.href = "javascript:void(0);"; floatButton.textContent = "(フロート表示)"; floatButton.style.marginLeft = "5px"; const container = document.createElement('div'); container.id = containerId; container.style.cssText = ` margin: 10px 0; position: relative; width: 100%; max-width: 640px; display: none; `; const iframe = document.createElement('iframe'); iframe.style.cssText = ` width: 100%; height: 360px; border: none; border-radius: 4px; `; iframe.src = `https://www.youtube.com/embed/${videoId}?rel=0&wmode=opaque`; iframe.setAttribute('allow', 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'); iframe.setAttribute('allowfullscreen', ''); toggle.addEventListener('click', (e) => { e.preventDefault(); container.style.display = container.style.display === 'none' ? 'block' : 'none'; iframe.src = iframe.src; }); floatButton.addEventListener('click', (e) => { e.preventDefault(); createFloatingWindow(videoId); }); container.appendChild(iframe); link.parentNode.insertBefore(toggle, link.nextSibling); link.parentNode.insertBefore(floatButton, toggle.nextSibling); link.parentNode.insertBefore(container, floatButton.nextSibling); processedLinks.add(link); } root.querySelectorAll('a[href*="youtube"], a[href*="youtu.be"]').forEach(processLink); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { node.querySelectorAll('a[href*="youtube"], a[href*="youtu.be"]').forEach(processLink); } }); }); }); observer.observe(root, { childList: true, subtree: true }); } function resolveOtherThreadLinks(root = document) { if (window.location.href.indexOf("/res/") === -1) return; const candidateLinks = root.querySelectorAll('a[href$=".htm"]'); candidateLinks.forEach(link => { if (link.closest('#contres')) return; let url = link.href.replace(/^http:\/\//, "https://"); link.href = url; if (link.dataset.resolvedThread) return; let hostname; try { hostname = new URL(url).hostname; } catch (e) { return; } if (!hostname.endsWith(".2chan.net") || !url.includes("/res/")) return; const idMatch = url.match(/\/res\/(\d+)\.htm$/); if (!idMatch) return; const threadId = idMatch[1]; if (resolvedThreadCache[url]) { const cache = resolvedThreadCache[url]; if (cache.type === 'original') { link.textContent = `>>${threadId} 「${cache.title}」 (別スレ)`; } else if (cache.type === 'archive') { link.href = cache.archiveUrl; link.textContent = `>>${threadId} 「${cache.title}」 (過去ログ)`; } else { link.textContent = `>>${threadId} (別スレ) (過去ログが見つかりません)`; } link.dataset.resolvedThread = "true"; return; } GM_xmlhttpRequest({ method: "GET", url: url + (url.includes('?') ? '&' : '?') + '_=' + Date.now(), onload: function(response) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const titleEl = doc.querySelector("title"); let title = titleEl ? titleEl.textContent.trim() : "タイトル取得失敗"; if (response.status === 200 && !title.includes("404 File Not Found")) { resolvedThreadCache[url] = { type: 'original', title: title }; updateLink(link, threadId, title, url, 'original'); } else { const archiveUrls = generateArchiveUrls(new URL(url).hostname, new URL(url).pathname.split('/')[1], threadId); checkArchiveUrls(archiveUrls, 0, function(archiveUrl, archiveTitle) { if (archiveUrl) { resolvedThreadCache[url] = { type: 'archive', title: archiveTitle, archiveUrl: archiveUrl }; updateLink(link, threadId, archiveTitle, archiveUrl, 'archive'); } else { resolvedThreadCache[url] = { type: 'not_found' }; updateLink(link, threadId, null, url, 'not_found'); } }); } }, onerror: function() { resolvedThreadCache[url] = { type: 'not_found' }; updateLink(link, threadId, null, url, 'not_found'); } }); }); } function generateArchiveUrls(host, board, threadId) { if (host.includes("may.2chan.net") && board === "b") { return [ `https://may1.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`, `https://may2.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`, `https://may3.ftbucket.info/may/cont/may.2chan.net_b_res_${threadId}/index.htm`, `https://kako.futakuro.com/futa/may_b/${threadId}/`, `https://futabaforest.net/b/res/${threadId}.htm` ]; } if (host.includes("img.2chan.net") && board === "b") { return [ `https://c3.ftbucket.info/img/cont/img.2chan.net_b_res_${threadId}/index.htm`, `https://kako.futakuro.com/futa/img_b/${threadId}/` ]; } if (host.includes("jun.2chan.net") && board === "jun") { return [ `https://c3.ftbucket.info/jun/cont/jun.2chan.net_jun_res_${threadId}/index.htm` ]; } if (host.includes("dec.2chan.net")) { return [ `https://c3.ftbucket.info/other/cont/${host.replace(/\./g, '_')}_${board}_res_${threadId}/index.htm` ]; } return []; } function checkArchiveUrls(urls, index, callback) { if (index >= urls.length) return callback(null, null); let options = { method: "HEAD", url: urls[index], onload: function(res) { if (res.status >= 200 && res.status < 400) { if (urls[index].includes("ftbucket.info")) { GM_xmlhttpRequest({ method: "GET", url: urls[index], responseType: "arraybuffer", onload: function(getRes) { let decoder = new TextDecoder("shift_jis"); let text = decoder.decode(getRes.response); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html"); const titleEl = doc.querySelector("title"); let title = titleEl ? titleEl.textContent.trim() : ""; if (!title || title === "タイトル不明") { checkArchiveUrls(urls, index + 1, callback); } else { callback(urls[index], title); } }, onerror: function() { checkArchiveUrls(urls, index + 1, callback); } }); } else if (urls[index].includes("futakuro.com")) { GM_xmlhttpRequest({ method: "GET", url: urls[index], onload: function(getRes) { const parser = new DOMParser(); const doc = parser.parseFromString(getRes.responseText, "text/html"); const titleEl = doc.querySelector("title"); let title = titleEl ? titleEl.textContent.trim() : ""; if (!title || title === "タイトル不明") { checkArchiveUrls(urls, index + 1, callback); } else { callback(urls[index], title); } }, onerror: function() { checkArchiveUrls(urls, index + 1, callback); } }); } else if (urls[index].includes("futabaforest.net")) { options.timeout = 10000; options.ontimeout = function() { checkArchiveUrls(urls, index + 1, callback); }; GM_xmlhttpRequest({ method: "GET", url: urls[index], responseType: "text", onload: function(getRes) { const parser = new DOMParser(); const doc = parser.parseFromString(getRes.responseText, "text/html"); const titleEl = doc.querySelector("title"); let title = titleEl ? titleEl.textContent.trim() : ""; if (!title || title === "タイトル不明") { checkArchiveUrls(urls, index + 1, callback); } else { callback(urls[index], title); } }, onerror: function() { checkArchiveUrls(urls, index + 1, callback); } }); } else { GM_xmlhttpRequest({ method: "GET", url: urls[index], onload: function(getRes) { const parser = new DOMParser(); const doc = parser.parseFromString(getRes.responseText, "text/html"); const titleEl = doc.querySelector("title"); let title = titleEl ? titleEl.textContent.trim() : ""; if (!title || title === "タイトル不明") { checkArchiveUrls(urls, index + 1, callback); } else { callback(urls[index], title); } }, onerror: function() { checkArchiveUrls(urls, index + 1, callback); } }); } } else { checkArchiveUrls(urls, index + 1, callback); } }, onerror: function() { checkArchiveUrls(urls, index + 1, callback); } }; GM_xmlhttpRequest(options); } function updateLink(link, threadId, title, newUrl, type) { if (type === 'original') { link.textContent = `>>${threadId} 「${title}」 (別スレ)`; } else if (type === 'archive') { link.href = newUrl; link.textContent = `>>${threadId} 「${title}」 (過去ログ)`; } else { link.textContent = `>>${threadId} (別スレ) (過去ログが見つかりません)`; } link.dataset.resolvedThread = "true"; } function processNodes(root = document) { removeUnwantedElements(root); addDownloadButtons(root); updateFileSizes(root); attachFullSizeHover(); changeFaviconIfCondition(); addYouTubeEmbeds(root); cleanseJumpLinks(root); insertQuoteBreaks(root); root.querySelectorAll("td.rtd").forEach(processPost); if (window.location.href.indexOf("/res/") !== -1) { resolveOtherThreadLinks(root); } } processNodes(); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { processNodes(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); setInterval(autoReloadThread, 5000); setInterval(checkSodCondition, 1000); if (window.location.href.indexOf("/res/") !== -1) { const scrollKey = "2chan_lastScroll_" + window.location.pathname; window.addEventListener("load", () => { const lastScroll = localStorage.getItem(scrollKey); if (lastScroll) { window.scrollTo(0, parseInt(lastScroll, 10)); } }); window.addEventListener("scroll", () => { localStorage.setItem(scrollKey, window.pageYOffset); }); } if (window.location.hostname === "www.2chan.net" && window.location.pathname === "/index2.html") { const boardMapping = { "img_b": "二次元裏img", "dec_dec": "二次元裏dec", "jun_jun": "二次元裏jun", "may_b": "二次元裏may", "dat_b": "二次元裏dat", "dec_58": "転載不可", "dec_59": "転載可", "dec_img2": "二次元表", "may_id": "二次元ID", "dat_43": "二次元業界", "dat_20": "甘味", "dat_21": "ラーメン", "dat_23": "スピグラ", "dat_l": "壁紙二", "may_25": "麻雀", "may_26": "うま", "may_27": "ねこ", "zip_12": "サッカー", "zip_14": "自作絵裏", "zip_5": "えろげ", "zip_1": "野球", "zip_11": "自作絵", "zip_2": "ろぼ", "dec_63": "映画", "zip_3": "自作PC", "zip_32": "女装", "zip_15": "ばら", "zip_7": "ゆり", "zip_8": "やおい", "dec_65": "刀剣乱舞", "dec_64": "占い", "dec_66": "ファッション", "dec_67": "旅行", "dec_68": "子育て", "may_webm": "webm", "dec_71": "そうだね", "zip_p": "お絵かき", "nov_q": "落書き", "zip_z": "しょくぶつ", "dat_d": "どうぶつ", "dat_e": "のりもの", "dat_j": "二輪", "nov_37": "自転車", "dat_45": "カメラ", "dat_48": "家電", "dat_r": "鉄道", "dat_t": "料理", "dat_44": "おもちゃ", "dat_v": "模型", "nov_y": "模型裏", "jun_47": "模型裏裏", "dat_w": "虫", "dat_49": "アクア", "dec_62": "アウトドア", "dec_73": "VTuber", "dec_84": "ホロライブ", "dec_81": "合成音声", "dat_x": "3DCG", "dec_85": "人工知能", "nov_35": "政治", "nov_36": "経済", "dec_79": "宗教", "dat_38": "尹錫悦", "dec_80": "岸田文雄", "dec_50": "三次実況", "cgi_f": "軍", "may_39": "軍裏", "dec_74": "FGO", "dec_75": "アイマス", "dec_86": "ZOIDS", "dec_78": "ウメハラ総合", "jun_31": "ゲーム", "nov_28": "ネトゲ", "dec_56": "ソシャゲ", "dec_60": "艦これ", "dec_82": "任天堂", "dec_69": "モアイ", "dec_61": "ソニー", "dat_10": "ネットキャラ", "nov_34": "なりきり", "cgi_g": "特撮", "cgi_i": "flash", "may_40": "東方", "dec_55": "東方裏", "cgi_k": "壁紙", "cgi_m": "数学", "zip_6": "ニュース表", "dec_76": "昭和", "dec_77": "平成", "dec_53": "発電", "dec_52": "自然災害", "dec_83": "コロナ", "img_9": "雑談", "dec_70": "新板提案", "cgi_o": "二次元グロ", "jun_51": "二次元グロ裏", "cgi_u": "落書き裏", "jun_oe": "お絵sql", "jun_72": "お絵sqlip", "www_hinan": "ふたば避難所", "jun_junbi": "準備" }; const RSS_URL = 'https://futapo.futakuro.com/ranking/index.rdf'; function fetchRSS() { GM_xmlhttpRequest({ method: 'GET', url: RSS_URL, onload: function(response) { if (response.status === 200) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(response.responseText, "text/xml"); const items = xmlDoc.getElementsByTagName('item'); let threads = []; for (let i = 0; i < Math.min(9, items.length); i++) { const item = items[i]; const titleRaw = item.getElementsByTagName('title')[0].textContent; const title = titleRaw.replace(/^\d+位[::]?/, '').trim(); let link = item.getElementsByTagName('link')[0].textContent; if(link.indexOf('//') === 0){ link = 'https:' + link; } let match = link.match(/https?:\/\/([^\.]+)\.2chan\.net\/([^\/]+)\/res\//); let subdomain = '', identifier = '', boardName = 'unknown'; if(match) { subdomain = match[1]; identifier = match[2]; let boardKey = subdomain + "_" + identifier; if(boardMapping[boardKey]){ boardName = boardMapping[boardKey]; } else { boardName = identifier; } } const descCDATA = item.getElementsByTagName('description')[0].textContent; let imgSrc = ''; const imgMatch = descCDATA.match(/<img\s+src=["']([^"']+)["']/); if (imgMatch) { imgSrc = imgMatch[1]; } threads.push({ subdomain, identifier, boardName, imgSrc, title, link }); } insertGrid(threads); } } }); } function insertGrid(threads) { const gridTable = document.createElement('table'); gridTable.style.margin = '20px auto'; gridTable.style.borderCollapse = 'collapse'; gridTable.style.fontFamily = 'sans-serif'; gridTable.style.fontSize = '10px'; gridTable.style.width = '70%'; gridTable.style.maxWidth = '800px'; gridTable.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)'; gridTable.style.border = '1px solid #000000'; gridTable.style.tableLayout = 'fixed'; const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); const headerCell = document.createElement('th'); headerCell.colSpan = 3; headerCell.textContent = '勢い上昇中のスレ'; headerCell.style.fontSize = '14px'; headerCell.style.fontWeight = 'bold'; headerCell.style.color = '#fff'; headerCell.style.backgroundColor = '#781225'; headerCell.style.padding = '2px 5px'; headerCell.style.textAlign = 'center'; headerRow.appendChild(headerCell); thead.appendChild(headerRow); gridTable.appendChild(thead); const tbody = document.createElement('tbody'); for (let row = 0; row < 3; row++) { const tr = document.createElement('tr'); for (let col = 0; col < 3; col++) { const td = document.createElement('td'); td.style.border = '1px solid #000000'; td.style.padding = '4px'; td.style.textAlign = 'center'; td.style.verticalAlign = 'top'; td.style.width = '33%'; td.style.boxSizing = 'border-box'; td.style.height = '200px'; td.style.overflow = 'hidden'; const index = row * 3 + col; if (threads[index]) { const { subdomain, identifier, boardName, imgSrc, title, link } = threads[index]; const boardLink = document.createElement('a'); boardLink.href = 'https://' + subdomain + '.2chan.net/' + identifier + '/'; boardLink.target = '_blank'; boardLink.textContent = boardName; boardLink.style.fontSize = '14px'; boardLink.style.fontWeight = 'bold'; boardLink.style.marginBottom = '2px'; boardLink.style.display = 'block'; td.appendChild(boardLink); const anchor = document.createElement('a'); anchor.href = link; anchor.target = '_blank'; anchor.style.textDecoration = 'none'; anchor.style.color = 'inherit'; if (imgSrc) { const imgElem = document.createElement('img'); imgElem.src = imgSrc; imgElem.style.maxWidth = '90%'; imgElem.style.maxHeight = '100px'; imgElem.style.height = 'auto'; imgElem.style.marginBottom = '2px'; anchor.appendChild(imgElem); } const titleElem = document.createElement('div'); titleElem.textContent = title; titleElem.style.marginTop = '2px'; titleElem.style.fontSize = '16px'; anchor.appendChild(titleElem); td.appendChild(anchor); } tr.appendChild(td); } tbody.appendChild(tr); } gridTable.appendChild(tbody); const existingTables = document.getElementsByTagName('table'); if (existingTables.length > 0) { const targetTable = existingTables[0]; const spacer = document.createElement('div'); spacer.style.height = '20px'; targetTable.parentNode.insertBefore(spacer, targetTable.nextSibling); targetTable.parentNode.insertBefore(gridTable, spacer.nextSibling); } } window.addEventListener('load', fetchRSS, false); } GM_addStyle(` .lds-ring, .lds-ring div { box-sizing: border-box; } .lds-ring { display: inline-block; position: relative; width: 14px; height: 14px; } .lds-ring div { box-sizing: border-box; display: block; position: absolute; width: 15px; height: 15px; border: 2px solid currentColor; border-radius: 50%; animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; border-color: currentColor transparent transparent transparent; } .lds-ring div:nth-child(1) { animation-delay: -0.45s; } .lds-ring div:nth-child(2) { animation-delay: -0.3s; } .lds-ring div:nth-child(3) { animation-delay: -0.15s; } @keyframes lds-ring { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `); if (!window.location.href.includes('futaba.php?mode=cat')) return; if (window.location.href.includes('mode=catset')) return; const urlParams = new URLSearchParams(window.location.search); const sortParam = urlParams.get('sort'); let existingThreads = new Set(); let fetchedThreads = []; let board = ''; let subdomain = ''; let useCatVersion = false; const catalogTable = document.querySelector('#cattable tbody'); if (!catalogTable) return; function recordExistingThreads() { catalogTable.querySelectorAll('td').forEach(td => { const link = td.querySelector('a[href*="res/"]'); if (link) { existingThreads.add(link.getAttribute('href')); } }); } recordExistingThreads(); let maxColumns = 1; const firstRow = catalogTable.querySelector('tr'); if (firstRow) { maxColumns = firstRow.children.length; } const firstImg = catalogTable.querySelector('td img'); if (firstImg && firstImg.src.includes('/cat/')) { useCatVersion = true; } const preExistingSmall = catalogTable.querySelector('td small'); const includeTitle = !!(preExistingSmall && preExistingSmall.textContent.trim().length > 0); let maxTitleLength = 100; if (preExistingSmall) { maxTitleLength = preExistingSmall.textContent.trim().length; } const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.placeholder = '絞り込み....'; searchBox.style.margin = '5px'; searchBox.style.padding = '3px'; searchBox.style.fontSize = '90%'; const targetLink = document.querySelector('a[href*="futaba.php?mode=cat"][href*="sort=9"][href*="guid=on"]'); if (targetLink) { targetLink.parentNode.insertBefore(searchBox, targetLink.nextSibling); } else { document.body.insertBefore(searchBox, document.body.firstChild); } searchBox.addEventListener('input', () => { const query = searchBox.value.trim().toLowerCase(); const tds = catalogTable.querySelectorAll('td'); tds.forEach(td => { if (query === '') { td.style.display = ''; } else { const small = td.querySelector('small'); const text = small ? small.textContent.toLowerCase() : ''; const href = td.querySelector('a') ? td.querySelector('a').getAttribute('href').toLowerCase() : ''; td.style.display = (text.includes(query) || href.includes(query)) ? '' : 'none'; } }); }); const loadingDialog = document.createElement('div'); loadingDialog.style.position = 'fixed'; loadingDialog.style.top = '46px'; loadingDialog.style.right = '20px'; loadingDialog.style.background = 'rgba(0,0,0,0.7)'; loadingDialog.style.color = '#fff'; loadingDialog.style.padding = '10px 15px'; loadingDialog.style.borderRadius = '5px'; loadingDialog.style.zIndex = '9999'; loadingDialog.style.fontSize = '14px'; loadingDialog.style.display = 'flex'; loadingDialog.style.alignItems = 'center'; loadingDialog.innerHTML = ` <div class="lds-ring"><div></div><div></div><div></div><div></div></div> <span id="loadingText" style="margin-left: 10px;">カタログを取得しています...</span> `; document.body.appendChild(loadingDialog); (function extractBoard() { const parts = window.location.hostname.split('.'); subdomain = parts[0]; const pathParts = window.location.pathname.split('/'); board = pathParts[1]; })(); function isInsideExcludedElement(el) { let p = el.parentElement; while (p) { if (['table', 'tr', 'td'].includes(p.tagName.toLowerCase())) return true; p = p.parentElement; } return false; } function createNewRow() { const tr = document.createElement('tr'); catalogTable.appendChild(tr); return tr; } let currentRow = catalogTable.lastElementChild || createNewRow(); function addThreadToDOM(thread) { if (existingThreads.has(thread.href)) return; existingThreads.add(thread.href); let titleHtml = ""; if (includeTitle && typeof thread.title === 'string' && thread.title.trim().length > 0) { let t = thread.title.trim(); if (t.length > maxTitleLength) { t = t.substring(0, maxTitleLength) + '...'; } titleHtml = `<br><small>${t}</small>`; } const td = document.createElement('td'); td.innerHTML = ` <a href="${thread.href}" target="_blank"> <img src="${thread.imgSrc}" border="0" width="${thread.width}" height="${thread.height}" alt="" loading="lazy"> </a> ${titleHtml} <br><font size="2">${thread.replyCount}</font><div class="pdmc" data-no="${thread.id}"></div> `; if (currentRow.children.length >= maxColumns) { currentRow = createNewRow(); } currentRow.appendChild(td); } function addThreadToCatalog(thread) { if (existingThreads.has(thread.href)) return; if (sortParam && !['6','7','8','9'].includes(sortParam)) { fetchedThreads.push(thread); return; } addThreadToDOM(thread); } function finalizeSortedThreads() { if (!sortParam || ['6','7','8','9'].includes(sortParam)) return; if (sortParam === '1') { fetchedThreads.sort((a, b) => new Date(b.date) - new Date(a.date)); fetchedThreads.reverse(); } else if (sortParam === '2') { fetchedThreads.sort((a, b) => new Date(a.date) - new Date(b.date)); fetchedThreads.reverse(); } else if (sortParam === '3') { fetchedThreads.sort((a, b) => b.replyCount - a.replyCount); } else if (sortParam === '4') { fetchedThreads.sort((a, b) => a.replyCount - b.replyCount); } fetchedThreads.forEach(thread => addThreadToDOM(thread)); } function gmFetch(url, attempts = 0) { const maxAttempts = 10; return new Promise(resolve => { function attempt() { GM_xmlhttpRequest({ method: 'GET', url: url, onload: resp => resolve(resp), onerror: err => { if (attempts < maxAttempts) { attempts++; console.warn(`Error fetching ${url}, attempt ${attempts}`, err); attempt(); } else { console.error(`Max attempts reached for ${url}. Returning empty`); resolve({ status: 408, responseText: "" }); } } }); } attempt(); }); } function fetchThreadPromise(threadURL, relativeHref) { return new Promise(resolve => { gmFetch(threadURL).then(response => { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const threadIDMatch = relativeHref.match(/res\/(\d+)\.htm/); const threadID = threadIDMatch ? threadIDMatch[1] : ""; let imgElement = null; const imgs = doc.querySelectorAll("img"); imgs.forEach(img => { if (!img.getAttribute("src").includes(`/${board}/thumb/`)) return; if (isInsideExcludedElement(img)) return; if (!imgElement) imgElement = img; }); if (!imgElement) { resolve(); return; } const thumbSrc = imgElement.getAttribute("src"); const catSrc = thumbSrc.replace(`/${board}/thumb/`, `/${board}/cat/`); const thumbWidth = imgElement.getAttribute("width") || "50"; const thumbHeight = imgElement.getAttribute("height") || "50"; const finalSrc = useCatVersion ? catSrc : thumbSrc; function processThread(finalW, finalH) { let blockquotes = doc.querySelectorAll("blockquote"); let threadText = ""; for (const bq of blockquotes) { if (isInsideExcludedElement(bq)) continue; threadText = bq.textContent.trim(); if (threadText) break; } let dateSpans = doc.querySelectorAll("span.cnw"); let threadDate = ""; for (const sp of dateSpans) { if (isInsideExcludedElement(sp)) continue; threadDate = sp.textContent.trim(); if (threadDate) break; } let replyCount = doc.querySelectorAll("table").length; let title = threadText.split("\n")[0] || ""; const threadInfo = { href: threadURL, id: threadID, imgSrc: finalSrc, width: finalW, height: finalH, title: title, date: threadDate, replyCount: replyCount }; addThreadToCatalog(threadInfo); resolve(); } if (useCatVersion) { let timedOut = false; const tId = setTimeout(() => { timedOut = true; console.warn(`Image load timeout for ${catSrc}, fallback size`); processThread(50, 50); }, 500); let tempImg = new Image(); tempImg.onload = () => { if (!timedOut) { clearTimeout(tId); processThread(tempImg.naturalWidth, tempImg.naturalHeight); } }; tempImg.onerror = () => { if (!timedOut) { clearTimeout(tId); processThread(50, 50); } }; tempImg.src = catSrc; } else { processThread(thumbWidth, thumbHeight); } }).catch(() => { resolve(); }); }); } function fetchIndexPagePromise(pageUrl) { console.log("Fetching index page:", pageUrl); return new Promise(resolve => { gmFetch(pageUrl).then(response => { if (response.status === 404 || response.responseText.includes("404 File Not Found")) { console.log(`Index page ${pageUrl} not found, skipping.`); resolve(); return; } const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); let threadPromises = []; const links = doc.querySelectorAll("a.hsbn"); links.forEach(link => { const relHref = link.getAttribute("href"); console.log("Found thread URL:", relHref, "on page:", pageUrl); const threadURL = `${window.location.protocol}//${subdomain}.2chan.net/${board}/` + relHref; if (!existingThreads.has(relHref)) { existingThreads.add(relHref); threadPromises.push(fetchThreadPromise(threadURL, relHref)); } }); Promise.all(threadPromises).then(resolve).catch(resolve); }).catch(resolve); }); } const baseURL = `${window.location.protocol}//${subdomain}.2chan.net/${board}/`; const indexPages = []; indexPages.push(baseURL + "futaba.htm"); for (let i = 1; i <= 20; i++) { indexPages.push(baseURL + i + ".htm"); } if (['6','7','8','9'].includes(sortParam)) { loadingDialog.style.display = "none"; setTimeout(() => { loadingDialog.style.display = "none"; }, 1500); return; } const indexPromises = indexPages.map(url => fetchIndexPagePromise(url)); Promise.all(indexPromises).then(() => { if (sortParam) { finalizeSortedThreads(); } loadingDialog.innerHTML = ` <div style="font-size: 14px; margin-right: 6px;">✅</div> <div id="loadingText" style="color: #fff; font-weight: bold;">カタログを取得しました!</div> `; loadingDialog.style.background = "green"; loadingDialog.style.opacity = "1"; setTimeout(() => { loadingDialog.style.display = "none"; }, 1500); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址