您需要先安装一个扩展,例如 篡改猴、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或关注我们的公众号极客氢云获取最新地址