您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ふたばちゃんねるのスレッド上で「あぷ」「あぷ小」の画像をプレビュー表示する
当前为
// ==UserScript== // @name Futaba Image Preview // @namespace http://2chan.net/ // @version 0.1.1 // @description ふたばちゃんねるのスレッド上で「あぷ」「あぷ小」の画像をプレビュー表示する // @author ame-chan // @match http://*.2chan.net/b/res/* // @match https://*.2chan.net/b/res/* // @match http://kako.futakuro.com/futa/* // @match https://kako.futakuro.com/futa/* // @match https://tsumanne.net/si/data/* // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net // @grant GM_xmlhttpRequest // @license MIT // @run-at document-idle // @connect 2chan.net // @connect *.2chan.net // @connect tsumanne.net // ==/UserScript== (async () => { 'use strict'; let initExecCreateLink = false; let initTimer; const addedStyle = `<style id="userjs-preview-style"> .zoom_button.not_copy_button { display: none; } .userjs-preview-link { padding-right: 24px; background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2038%2038%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23000%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20transform%3D%22translate(1%201)%22%20stroke-width%3D%222%22%3E%3Ccircle%20stroke-opacity%3D%22.5%22%20cx%3D%2218%22%20cy%3D%2218%22%20r%3D%2218%22%2F%3E%3Cpath%20d%3D%22M36%2018c0-9.94-8.06-18-18-18%22%3E%20%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20from%3D%220%2018%2018%22%20to%3D%22360%2018%2018%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'); background-repeat: no-repeat; background-position: right center; } .userjs-preview-imageWrap { max-width: calc(100vw - 200px); width: fit-content; } .userjs-preview-image { max-width: calc(100vw - 200px) !important; max-height: none !important; transition: all 0.2s ease-in-out; border-radius: 4px; cursor: pointer; } .userjs-preview-title { display: flex; flex-direction: row; margin: 8px 0 16px; gap: 16px; padding: 16px; line-height: 1.6 !important; color: #ff3860 !important; background-color: #fff; border-radius: 4px; } </style>`; if (!document.querySelector('#userjs-preview-style')) { document.head.insertAdjacentHTML('beforeend', addedStyle); } class FileReaderEx extends FileReader { constructor() { super(); } #readAs(blob, ctx) { return new Promise((res, rej) => { super.addEventListener('load', ({ target }) => target?.result && res(target.result)); super.addEventListener('error', ({ target }) => target?.error && rej(target.error)); super[ctx](blob); }); } readAsArrayBuffer(blob) { return this.#readAs(blob, 'readAsArrayBuffer'); } readAsDataURL(blob) { return this.#readAs(blob, 'readAsDataURL'); } } // あぷ・あぷ小ファイルの文字列を見つけたらリンクに変換する(既にリンクになってたらスキップする) const createAnchorLink = (elms) => { const processNode = (node) => { const regex = /((?<!<a[^>]*>)(fu?)([0-9]{5,8})\.(jpe?g|png|webp|gif|bmp)(?![^<]*<\/a>))/g; if (node.nodeType === 3) { let textNode = node; let match; while ((match = regex.exec(textNode.data)) !== null) { const [fullMatch, _, type, digits, ext] = match; console.log('match:', match); const url = type === 'fu' ? `//dec.2chan.net/up2/src/${type}${digits}.${ext}` : `//dec.2chan.net/up/src/${type}${digits}.${ext}`; const anchor = document.createElement('a'); anchor.href = url; anchor.dataset.from = 'userjs-preview'; anchor.textContent = fullMatch; const nextTextNode = textNode.splitText(match.index); nextTextNode.data = nextTextNode.data.substring(fullMatch.length); textNode.parentNode.insertBefore(anchor, nextTextNode); textNode = nextTextNode; } } else if (node.nodeType !== 1 || node.tagName !== 'BR') { const childNodes = Array.from(node.childNodes); childNodes.forEach((childNode) => processNode(childNode)); } }; for (const el of elms) { processNode(el); } }; const fetchData = (url, responseType) => new Promise((resolve) => { let options = { method: 'GET', url, timeout: 10000, onload: (result) => { if (result.status === 200) { return resolve(result.response); } return resolve(false); }, onerror: () => resolve(false), ontimeout: () => resolve(false), }; if (typeof responseType === 'string') { options = { ...options, responseType, }; } GM_xmlhttpRequest(options); }); const setFailedText = (linkElm) => { if (linkElm && linkElm instanceof HTMLAnchorElement) { linkElm.insertAdjacentHTML('afterend', '<span class="userjs-preview-title">データ取得失敗</span>'); } }; const setImageElm = async (blob, linkElm) => { const imageMinSize = 480; const imageMaxSize = 1024; const imageEventHandler = (e) => { const self = e.currentTarget; const div = self?.parentElement; if (!(self instanceof HTMLImageElement) || !div) return; if (self.width === imageMinSize) { self.width = self.naturalWidth > imageMaxSize ? self.naturalWidth : imageMaxSize; } else { self.width = imageMinSize; } }; const dataUrl = await new FileReaderEx().readAsDataURL(blob); const div = document.createElement('div'); div.classList.add('userjs-preview-imageWrap'); const img = document.createElement('img'); img.addEventListener('load', () => { if (img.naturalWidth < imageMinSize) { img.width = img.naturalWidth; } }); img.src = dataUrl; img.width = imageMinSize; img.classList.add('userjs-preview-image'); div.appendChild(img); img.addEventListener('click', imageEventHandler); linkElm.insertAdjacentElement('afterend', div); return img; }; const setLoading = (linkElm) => { const parentElm = linkElm.parentElement; if (parentElm instanceof HTMLFontElement) { return; } linkElm.classList.add('userjs-preview-link'); }; const removeLoading = (targetElm) => targetElm.classList.remove('userjs-preview-link'); // ふたクロで「新着レスに自動スクロール」にチェックが入っている場合画像差し込み後に下までスクロールさせる const scrollIfAutoScrollIsEnabled = () => { const checkboxElm = document.querySelector('#autolive_scroll'); const readmoreElm = document.querySelector('#res_menu'); if (checkboxElm === null || readmoreElm === null || !checkboxElm?.checked) { return; } const elementHeight = readmoreElm.offsetHeight; const viewportHeight = window.innerHeight; const offsetTop = readmoreElm.offsetTop; window.scrollTo({ top: offsetTop - viewportHeight + elementHeight, behavior: 'smooth', }); }; const insertURLData = async (linkElm) => { const parentElm = linkElm.parentElement; if (parentElm instanceof HTMLFontElement) { removeLoading(linkElm); return; } const data = await fetchData(linkElm.href, 'blob'); if (!data) { setFailedText(linkElm); removeLoading(linkElm); return; } const imageElm = await setImageElm(data, linkElm); if (imageElm instanceof HTMLImageElement) { imageElm.onload = () => scrollIfAutoScrollIsEnabled(); } removeLoading(linkElm); }; const searchLinkElements = async (targetElm) => { const processBatch = async (batch) => { const promises = batch.map(async (linkElm) => { if (!linkElm.classList.contains('userjs-preview-link')) return; await insertURLData(linkElm); }); await Promise.all(promises); }; const linkElms = targetElm.querySelectorAll('a[href*="2chan.net/up"], a[href^="f"]'); if (!linkElms.length) return; const regExp = /(tsumanne\.net\/si\/data|\w+\.2chan\.net\/up[0-9]?\/src)\/(.+?)\.(jpe?g|png|gif|webp)/; const isMatch = (ele) => regExp.test(ele.href); for (const linkElm of linkElms) { if (!isMatch(linkElm)) continue; setLoading(linkElm); } for (let i = 0; i < linkElms.length; i += 5) { const linkElm = linkElms[i]; if (!isMatch(linkElm)) continue; const batch = Array.from(linkElms).slice(i, i + 5); await processBatch(batch); } }; const deleteDuplicate = (blockquoteElms) => { for (const blockquoteElm of blockquoteElms) { const anchorElms = blockquoteElm.querySelectorAll('a[data-orig]'); for (const anchorElm of anchorElms) { const newAnchorElm = anchorElm.querySelector('a[data-from]'); if (newAnchorElm !== null) { anchorElm.outerHTML = newAnchorElm.outerHTML; } } } }; const mutationLinkElements = async (mutations) => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (!(addedNode instanceof HTMLElement)) continue; const newBlockQuotes = addedNode.querySelectorAll('blockquote'); createAnchorLink(newBlockQuotes); deleteDuplicate(newBlockQuotes); searchLinkElements(addedNode); } } }; // ふたクロが無い環境用にアンカーリンクを生成したい const exec = () => { const threadElm = document.querySelector('.thre'); const futakuroElm = document.querySelector('#fvw_menu'); if (!initExecCreateLink && threadElm instanceof HTMLElement && futakuroElm === null) { const quoteElms = threadElm.querySelectorAll('blockquote'); initExecCreateLink = true; if (initTimer) { clearTimeout(initTimer); } createAnchorLink(quoteElms); searchLinkElements(threadElm); } }; let threadElm = document.querySelector('.thre'); if (threadElm instanceof HTMLElement) { searchLinkElements(threadElm); const observer = new MutationObserver(mutationLinkElements); observer.observe(threadElm, { childList: true, }); initTimer = setTimeout(exec, 1500); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址