您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ふたばちゃんねるのスレッド上で「あぷ」「あぷ小」の画像をプレビュー表示する
// ==UserScript== // @name futaba-image-preview // @namespace http://2chan.net/ // @version 0.7.4 // @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 // @grant GM_setValue // @grant GM_getValue // @license MIT // @run-at document-idle // @connect 2chan.net // @connect img.2chan.net // @connect dec.2chan.net // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/exif-reader.min.js // ==/UserScript== (async () => { 'use strict'; const resNumberStorage = {}; 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-inner { position: relative; width: fit-content; } .userjs-preview-inner.is-caution::after { position: absolute; top: 0; left: 0; content: ""; display: block; width: 100%; height: 100%; backdrop-filter: blur(40px); border-radius: 4px; } .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-close { position: absolute; top: -12px; right: -12px; width: 24px; height: 24px; line-height: 1; color: #fff; background-color: hsl(347.94deg 100% 60.98%); border: none; border-radius: 50%; cursor: pointer; z-index: 10; } .userjs-preview-close:hover { background-color: hsl(347.94deg 100% 75.98%); } .userjs-preview-close::before { position: absolute; top: 50%; left: 50%; content: ""; display: block; width: 16px; height: 16px; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path fill="%23FFFFFF" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>'); background-repeat: no-repeat; background-size: 16px 16px; transform: translate3d(-50%, -50%, 0); } .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; } .userjs-prompt { color: #888; border-left: 4px solid #888; padding: 0 0 0 16px; } .fat-settings button + p { margin-top: 16px; } </style>`; const settingsStyle = `<style id="fat-style"> .fat-icon { position: fixed; right: 16px; bottom: 16px; padding: 8px; width: 24px; height: 24px; z-index: 9999; background-color: #fff; border-radius: 50%; box-shadow: 0 2px 10px rgb(0 0 0 / 30%); cursor: pointer; } .fat-icon::before { display: block; width: 24px; height: 24px; content: ""; background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='100px' height='100px'%3E%3Cpath d='M47.16,21.221l-5.91-0.966c-0.346-1.186-0.819-2.326-1.411-3.405l3.45-4.917c0.279-0.397,0.231-0.938-0.112-1.282 l-3.889-3.887c-0.347-0.346-0.893-0.391-1.291-0.104l-4.843,3.481c-1.089-0.602-2.239-1.08-3.432-1.427l-1.031-5.886 C28.607,2.35,28.192,2,27.706,2h-5.5c-0.49,0-0.908,0.355-0.987,0.839l-0.956,5.854c-1.2,0.345-2.352,0.818-3.437,1.412l-4.83-3.45 c-0.399-0.285-0.942-0.239-1.289,0.106L6.82,10.648c-0.343,0.343-0.391,0.883-0.112,1.28l3.399,4.863 c-0.605,1.095-1.087,2.254-1.438,3.46l-5.831,0.971c-0.482,0.08-0.836,0.498-0.836,0.986v5.5c0,0.485,0.348,0.9,0.825,0.985 l5.831,1.034c0.349,1.203,0.831,2.362,1.438,3.46l-3.441,4.813c-0.284,0.397-0.239,0.942,0.106,1.289l3.888,3.891 c0.343,0.343,0.884,0.391,1.281,0.112l4.87-3.411c1.093,0.601,2.248,1.078,3.445,1.424l0.976,5.861C21.3,47.647,21.717,48,22.206,48 h5.5c0.485,0,0.9-0.348,0.984-0.825l1.045-5.89c1.199-0.353,2.348-0.833,3.43-1.435l4.905,3.441 c0.398,0.281,0.938,0.232,1.282-0.111l3.888-3.891c0.346-0.347,0.391-0.894,0.104-1.292l-3.498-4.857 c0.593-1.08,1.064-2.222,1.407-3.408l5.918-1.039c0.479-0.084,0.827-0.5,0.827-0.985v-5.5C47.999,21.718,47.644,21.3,47.16,21.221z M25,32c-3.866,0-7-3.134-7-7c0-3.866,3.134-7,7-7s7,3.134,7,7C32,28.866,28.866,32,25,32z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: cover; transition: all 0.3s ease; transform: rotate(0deg); } .fat-icon:hover::before { transform: rotate(180deg); } .fat-settings { position: fixed; bottom: 72px; right: 16px; display: flex; flex-direction: column; padding: 16px; max-width: 80%; width: calc(350px - 32px); height: fit-content; color: #202020; background-color: #fff; border-radius: 6px; transition: transform 0.3s ease; transform: translateX(400px); z-index: 10001; } .fat-settings p { margin: 0; padding: 0; font-size: 16px; } .fat-settings button + p { margin-top: 16px; } .fat-settings p span { font-size: 13px; } .fat-settings textarea { margin-top: 8px; padding: 8px; height: 150px; max-height: 400px; min-height: 100px; line-height: 1.3; letter-spacing: 0.5px; font-weight: 400; font-family: Verdana; border-radius: 4px; border: 1px solid #ccc; resize: vertical; } .fat-settings button { margin-top: 16px; padding: 8px 16px; width: fit-content; color: #fff; font-size: 13px; border: 0px; border-radius: 4px; background-color: #00d1b2; appearance: none; cursor: pointer; } .fat-settings button:hover { filter: saturate(130%); } .fat-settings button:active { filter: saturate(150%); } .fat-settings.is-visible { transform: translateX(0); } </style>`; if (!document.querySelector('#userjs-preview-style')) { document.head.insertAdjacentHTML('beforeend', addedStyle); } if (!document.querySelector('#fat-style')) { document.head.insertAdjacentHTML('beforeend', settingsStyle); } const getCloseFileName = async () => JSON.parse((await GM_getValue('closeFileName')) || '[]'); const getMinSize = async () => { const defaultValue = '480'; const storageValue = await GM_getValue('minSize'); return storageValue || defaultValue; }; const hasFutakuroElm = () => document.querySelector('#fvw_menu') !== null; // あぷ・あぷ小ファイルの文字列を見つけたらリンクに変換する(既にリンクになってたらスキップする) 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; // テキストノードの親要素がaタグである場合、処理をスキップ if (textNode.parentNode?.nodeName === 'A') { return; } let match; while ((match = regex.exec(textNode.data)) !== null) { const [fullMatch, _, type, digits, ext] = 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.classList.add('is-createLink'); 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 setFailedText = (linkElm) => { if (linkElm && linkElm instanceof HTMLAnchorElement) { linkElm.insertAdjacentHTML('afterend', '<span class="userjs-preview-title">データ取得失敗</span>'); } }; const getArrayBuffer = (path) => { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: path, responseType: 'blob', onload: ({ response }) => { return resolve(response); }, onerror: (error) => { console.log(error); }, }); }); }; 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 getPromptData = async (div, img) => { const getGridPosition = ({ x, y }) => { const xPos = Math.round((x - 0.1) / 0.2); const yPos = Math.round((y - 0.1) / 0.2); const column = String.fromCharCode(65 + xPos); const row = yPos + 1; return `${column}${row}`; }; const escapeHtml = (str) => { if (!str) return ''; return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; const createV4Prompt = (v4_prompt) => { const charCaptions = v4_prompt?.caption?.char_captions; let text = escapeHtml(v4_prompt?.caption?.base_caption) || ''; if (charCaptions) { text += '<br>'; text += charCaptions.reduce((temp, data, idx) => { temp += `<strong>▼character prompt ${idx + 1}</strong><br>`; temp += escapeHtml(data.char_caption) + '<br>'; if (data.centers && data.centers.length) { for (const center of data.centers) { temp += `<strong>position</strong><br>`; temp += `${getGridPosition(center)}<br>`; } } return temp; }, ''); } return text; }; const promptParser = (tags) => { const parse = JSON.parse(tags?.['Comment']?.value || '{}'); if (Object.keys(parse).length === 0) { if (img.src.endsWith('.webp')) { const webpParseArray = tags?.['UserComment']?.value || []; const text = webpParseArray .filter((code) => code !== 0) .map((code) => String.fromCharCode(code)) .join(''); const match = text.match(/Comment: ({.+})/); if (match && JSON.parse(match[1])) { return JSON.parse(match[1]); } } return {}; } return parse; }; try { const isExifImageType = img.src.endsWith('.png') || img.src.endsWith('.webp'); if (!isExifImageType || !window.ExifReader) return; const buffer = await getArrayBuffer(img.src); const data = await new FileReaderEx().readAsDataURL(buffer); const tags = await window.ExifReader.load(data); const parse = promptParser(tags); const { prompt, v4_prompt } = parse; if (!prompt && !v4_prompt) return; const p = document.createElement('p'); let html = prompt; p.classList.add('userjs-prompt'); if (v4_prompt) { html = createV4Prompt(v4_prompt); } if (prompt || v4_prompt) { p.innerHTML = html; } else { p.textContent = 'プロンプトがありません'; } div.appendChild(p); } catch (error) { console.error(error); } }; const wrapperClickHandler = (e) => { const self = e.currentTarget; if (self.classList.contains('is-caution')) { self.classList.remove('is-caution'); } }; const makeCloseButton = () => { const closeButtonElm = document.createElement('button'); closeButtonElm.classList.add('userjs-preview-close'); return closeButtonElm; }; const setSetting = async () => { const delay = (time = 500) => new Promise((resolve) => setTimeout(() => resolve(true), time)); const value = await getMinSize(); const toggleSetting = () => { const settingElm = document.querySelector('[data-fat="settings"]'); settingElm?.classList.toggle('is-visible'); }; const saveSetting = async () => { const minSizeElm = document.querySelector(`[data-fat="minSize"]`); if (!minSizeElm) return; const value = minSizeElm.value; if (value === '' || /^[0-9]+$/.test(value) === false) { alert('数値を入力してください'); return; } await GM_setValue('minSize', value); const settingElm = document.querySelector('[data-fat="settings"]'); settingElm?.classList.remove('is-visible'); await delay(300); location.reload(); }; const iconHTML = `<div class="fat-icon" data-fat="icon"></div>`; const settingInnerHTML = `<p>デフォルトの画像サイズを指定、0で非表示</p> <input type="text" data-fat="minSize" value="${value}"> <button type="button" data-fat="minSizeSave">条件を保存してリロード</button>`; const settingHTML = `<div class="fat-settings" data-fat="settings"> ${settingInnerHTML} </div>`; await delay(1000); const hasFatIconElm = document.querySelector('[data-fat="icon"]') !== null; const fatSettingsElm = document.querySelector('[data-fat="settings"]'); if (!hasFatIconElm) { document.body.insertAdjacentHTML('afterbegin', iconHTML); await delay(100); const settingIconElm = document.querySelector(`[data-fat="icon"]`); settingIconElm?.addEventListener('click', toggleSetting); } if (fatSettingsElm !== null) { fatSettingsElm.insertAdjacentHTML('beforeend', settingInnerHTML); await delay(100); const settingSaveElm = document.querySelector(`[data-fat="minSizeSave"]`); settingSaveElm?.addEventListener('click', saveSetting); } else { document.body.insertAdjacentHTML('afterbegin', settingHTML); await delay(100); const settingSaveElm = document.querySelector(`[data-fat="minSizeSave"]`); settingSaveElm?.addEventListener('click', saveSetting); } }; const closeBtnEventHandler = async (e, div, fileName) => { e.stopPropagation(); div.remove(); const data = await getCloseFileName(); if (!data.includes(fileName)) { data.push(fileName); GM_setValue('closeFileName', JSON.stringify(data)); } }; const setImageElm = async (linkElm) => { const imageMinSize = Number(await getMinSize()); const imageMaxSize = 1024; const imageEventHandler = (e) => { const self = e.currentTarget; if (!(self instanceof HTMLImageElement)) return; const naturalWidth = self.naturalWidth; if (naturalWidth < imageMinSize) { self.width = self.width === naturalWidth ? imageMinSize : naturalWidth; } else if (self.width === imageMinSize) { self.width = naturalWidth > imageMaxSize ? naturalWidth : imageMaxSize; } else { self.width = imageMinSize; } }; const fileName = (linkElm.textContent || '').trim(); const closeFileName = await getCloseFileName(); if (closeFileName.includes(fileName)) { return; } const resText = linkElm.closest('blockquote')?.textContent; const div = document.createElement('div'); const innerDiv = document.createElement('div'); div.classList.add('userjs-preview-imageWrap'); innerDiv.classList.add('userjs-preview-inner'); if (/注意|グロ/g.test(resText || '')) { innerDiv.classList.add('is-caution'); } const img = document.createElement('img'); const closeBtnElm = makeCloseButton(); img.addEventListener('load', () => { if (img.naturalWidth < imageMinSize) { img.width = img.naturalWidth; } getPromptData(div, img); }); img.addEventListener('error', () => setFailedText(linkElm)); img.src = linkElm.href; img.width = imageMinSize; img.classList.add('userjs-preview-image'); innerDiv.appendChild(img); innerDiv.appendChild(closeBtnElm); div.appendChild(innerDiv); img.addEventListener('click', imageEventHandler); innerDiv.addEventListener('click', wrapperClickHandler); closeBtnElm.addEventListener('click', (e) => closeBtnEventHandler(e, div, fileName)); linkElm.insertAdjacentElement('afterend', div); return img; }; const setLoading = async (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 setResNumber = (linkElm, fileName) => { const tdElm = linkElm.closest('td.rtd'); const resNumber = tdElm?.querySelector('.rsc'); if (resNumber && resNumber.textContent) { const num = Number(resNumber.textContent); const storage = resNumberStorage[num]; if (Number.isInteger(num) && fileName) { if (typeof storage === 'undefined') { resNumberStorage[num] = [fileName]; } else if (Array.isArray(storage) && !storage.includes(fileName)) { storage.push(fileName); } } } }; const isFindFileNameFromStorage = (fileName) => Object.keys(resNumberStorage).some((key) => { const arr = resNumberStorage?.[Number(key)]; return arr && fileName && arr.includes(fileName); }); const insertURLData = async (linkElm, match) => { const [, , , fileName] = match; const imageElm = await setImageElm(linkElm); if (imageElm instanceof HTMLImageElement) { linkElm.classList.add('is-intersecting'); setResNumber(linkElm, fileName); imageElm.onload = () => scrollIfAutoScrollIsEnabled(); } removeLoading(linkElm); }; const linkRegExp = /((tsumanne\.net\/si\/data|\w+\.2chan\.net\/up[0-9]?\/src)\/)?(fu?[0-9]{5,8}\.(jpe?g|png|gif|webp|bmp))/; class LinkObserver { targetLink; isObserving; matchLink; options; constructor(targetLink) { this.targetLink = targetLink; this.isObserving = this.targetLink.classList.contains('is-observing'); this.matchLink = this.targetLink.href.match(linkRegExp); this.options = { rootMargin: '800px 0px 0px 0px', }; } observer() { return new IntersectionObserver(async ([entry], observer) => { if (entry.isIntersecting) { observer.disconnect(); const linkElm = entry.target; if (this.matchLink && linkElm instanceof HTMLAnchorElement) { await setLoading(linkElm); if (linkElm.classList.contains('userjs-preview-link')) { await insertURLData(linkElm, this.matchLink); } } } }, this.options); } check() { if (this.matchLink === null) { return false; } const isQuoteText = this.targetLink.closest('font[color="#789922"]') !== null; const [, , , fileName] = this.matchLink; if (isQuoteText || isFindFileNameFromStorage(fileName)) { return false; } return true; } init() { const isCheckOK = this.check(); if (isCheckOK && !this.isObserving && this.matchLink) { this.targetLink.classList.add('is-observing'); this.observer().observe(this.targetLink); } } } const getLinkElm = (threElm) => { const linkElms = threElm.querySelectorAll('a[href*="2chan.net/up"], a[href^="f"]'); if (linkElms.length) { return linkElms; } return []; }; 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 setLinkObserver = (linkElms) => { for (const linkElm of linkElms) { if (linkElm instanceof HTMLAnchorElement) { const linkObserver = new LinkObserver(linkElm); linkObserver.init(); } } }; const mutationLinkElements = async (mutations) => { const futakuroState = hasFutakuroElm(); for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (!(addedNode instanceof HTMLElement)) continue; const newBlockQuotes = addedNode.querySelectorAll('blockquote'); if (!futakuroState) { createAnchorLink(newBlockQuotes); deleteDuplicate(newBlockQuotes); } for (const newBlockQuote of newBlockQuotes) { const linkElms = newBlockQuote.querySelectorAll('a'); if (linkElms.length) { setLinkObserver(linkElms); } } } } }; // ふたクロが無い環境用にアンカーリンクを生成したい const exec = () => { const threadElm = document.querySelector('.thre'); const isTsumanne = location.hostname === 'tsumanne.net'; const isFutakuro = location.hostname === 'kako.futakuro.com'; if (!isTsumanne && !isFutakuro && !hasFutakuroElm() && !initExecCreateLink && threadElm instanceof HTMLElement) { const quoteElms = threadElm.querySelectorAll('blockquote'); initExecCreateLink = true; if (initTimer) { clearTimeout(initTimer); } createAnchorLink(quoteElms); for (const quoteElm of quoteElms) { const linkElms = quoteElm.querySelectorAll('.is-createLink'); setLinkObserver(linkElms); } } }; const threadElm = document.querySelector('.thre'); if (threadElm instanceof HTMLElement) { setSetting(); const linkElms = getLinkElm(threadElm); setLinkObserver(linkElms); const observer = new MutationObserver(mutationLinkElements); observer.observe(threadElm, { childList: true, subtree: true, }); initTimer = setTimeout(exec, 1500); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址