// ==UserScript== // @name ふたば 被引用情報表示スクリプト // @namespace http://tampermonkey.net/ // @version 1.7 // @description ふたばの被引用情報を表示。 // @author ぶいぶい // @match *://*.2chan.net/* // @grant none // ==/UserScript== (function() { 'use strict'; // --- 設定 --- const CONFIG = { pollingInterval: 2000, hoverDelay: 300, tooltipStay: 400, hoverBg: '#FFFFEE', inlineBg: '#F0E0D6', borderColor: '#800000', previewImgSize: 500 }; let textQuoteMap = {}; let fileQuoteMap = {}; let quoteMap = {}; const PROCESSED_CLASS = 'quote-script-processed'; function updateDataAndUI() { if (document.hidden) return; const posts = document.querySelectorAll('.thre, table'); posts.forEach(container => { const idElement = container.querySelector('.cno'); if (!idElement) return; const postId = idElement.textContent.replace(/^No\./i, ''); container.id = `resContainer-${postId}`; const fileLink = container.querySelector('a[href*="/b/src/"]'); if (fileLink) { const fname = fileLink.textContent; if (fname.match(/\.(jpg|png|gif|webm|mp4)$/i)) fileQuoteMap[fname] = postId; } const blockquote = container.querySelector('blockquote'); if (blockquote) { const text = blockquote.innerText || blockquote.textContent; text.split('\n').forEach(line => { const trimmed = line.trim(); if (trimmed) textQuoteMap[trimmed] = postId; }); } }); document.querySelectorAll(`td.rtd:not(.${PROCESSED_CLASS})`).forEach(replyContainer => { replyContainer.classList.add(PROCESSED_CLASS); const container = replyContainer.closest('[id^="resContainer-"]'); if (!container) return; const currentPostId = container.id.replace('resContainer-', ''); const currentIdNum = parseInt(currentPostId, 10); // 数値化 replyContainer.querySelectorAll('font[color="#789922"]').forEach(font => { font.innerHTML.split(//i).forEach(quoteLine => { let clean = quoteLine.replace(/>/g, '>').replace(/<[^>]*>/g, '').trim().replace(/^[>>]+/, '').trim(); if (!clean) return; let targetId = null; if (/^No\.\d+$/.test(clean)) targetId = clean.replace('No.', ''); else if (/^\d+$/.test(clean)) targetId = clean; else if (fileQuoteMap[clean]) targetId = fileQuoteMap[clean]; else if (textQuoteMap[clean]) targetId = textQuoteMap[clean]; if (targetId && targetId !== currentPostId) { const targetIdNum = parseInt(targetId, 10); if (!isNaN(targetIdNum) && currentIdNum > targetIdNum) { if (!quoteMap[targetId]) quoteMap[targetId] = []; if (!quoteMap[targetId].includes(currentPostId)) { quoteMap[targetId].push(currentPostId); } } } }); }); }); for (const quotedPostId in quoteMap) { const quotingPosts = quoteMap[quotedPostId]; const targetPost = document.getElementById(`resContainer-${quotedPostId}`); if (!targetPost) continue; const idElement = targetPost.querySelector('.cno'); if (!idElement) continue; let link = targetPost.querySelector('.quote-check-trigger'); const newTextBase = ` (被引用:${quotingPosts.length})`; const newDataset = JSON.stringify(quotingPosts); if (!link) { link = document.createElement('span'); link.className = 'quote-check-trigger'; link.style.cssText = 'color: navy; cursor: pointer; text-decoration: underline; font-size: small; margin-left: 4px; user-select: none;'; link.textContent = newTextBase; link.dataset.postIds = newDataset; link.dataset.targetId = quotedPostId; idElement.insertAdjacentElement('afterend', link); } else if (link.dataset.postIds !== newDataset) { link.dataset.postIds = newDataset; if (link.textContent.includes('▼')) { link.textContent = newTextBase + ' ▼'; const wrapper = document.getElementById(`inline-wrapper-${quotedPostId}`); if (wrapper && wrapper.style.display !== 'none') { const content = wrapper.querySelector('.inline-list-container'); if (content) content.innerHTML = buildPreviewHTML(quotingPosts, true); } } else { link.textContent = newTextBase; } } } } setInterval(updateDataAndUI, CONFIG.pollingInterval); window.addEventListener('load', updateDataAndUI); document.addEventListener('visibilitychange', () => { if (!document.hidden) updateDataAndUI(); }); let tooltipEl = null; let imgPreviewEl = null; let showTimer = null; let hideTimer = null; document.body.addEventListener('click', (e) => { if (e.target.classList.contains('quote-check-trigger')) { e.preventDefault(); e.stopPropagation(); handleInlineToggle(e.target); } else if (e.target.classList.contains('quote-filter-checkbox')) { handleFilterChange(e.target); } }, true); document.body.addEventListener('mouseover', (e) => { if (e.target.classList.contains('quote-check-trigger')) { clearTimeout(hideTimer); const trigger = e.target; if (trigger.textContent.includes('▼')) return; showTimer = setTimeout(() => createTooltip(trigger), CONFIG.hoverDelay); } else if (tooltipEl && (e.target === tooltipEl || tooltipEl.contains(e.target))) { clearTimeout(hideTimer); } else if (e.target.classList.contains('quote-thumb-img')) { showImagePreview(e.target); } }); document.body.addEventListener('mouseout', (e) => { if (e.target.classList.contains('quote-check-trigger')) { clearTimeout(showTimer); hideTimer = setTimeout(removeTooltip, CONFIG.tooltipStay); } else if (tooltipEl && (e.target === tooltipEl || tooltipEl.contains(e.target))) { if (!e.relatedTarget || !e.relatedTarget.classList.contains('quote-check-trigger')) { hideTimer = setTimeout(removeTooltip, CONFIG.tooltipStay); } } else if (e.target.classList.contains('quote-thumb-img')) { hideImagePreview(); } }); function handleFilterChange(checkbox) { const wrapper = checkbox.closest('.quote-wrapper-box'); if (!wrapper) return; const listContainer = wrapper.querySelector('.inline-list-container'); if (checkbox.checked) listContainer.classList.add('filter-images-only'); else listContainer.classList.remove('filter-images-only'); } const style = document.createElement('style'); style.textContent = ` .filter-images-only .quote-preview-item:not(.has-image) { display: none !important; } .quote-preview-item { border-bottom: 1px solid #ccc; padding: 5px 0; font-size: 12px; display: flow-root; position: relative; } .quote-sub-wrapper { margin-top: 8px; padding: 5px; border-left: 2px solid #800000; background: #fff; display: block; clear: both; } `; document.head.appendChild(style); function createTooltip(trigger) { removeTooltip(); if (!trigger.dataset.postIds) return; const ids = JSON.parse(trigger.dataset.postIds); tooltipEl = document.createElement('div'); tooltipEl.className = 'quote-wrapper-box'; tooltipEl.style.cssText = ` position: absolute; z-index: 9000; background: ${CONFIG.hoverBg}; border: 1px solid ${CONFIG.borderColor}; padding: 8px; border-radius: 4px; box-shadow: 2px 2px 10px rgba(0,0,0,0.3); max-width: 450px; font-size: 12px; pointer-events: auto; `; tooltipEl.innerHTML = buildHeaderHTML(ids.length) + `
${buildPreviewHTML(ids, true)}
`; document.body.appendChild(tooltipEl); const rect = trigger.getBoundingClientRect(); tooltipEl.style.top = `${window.scrollY + rect.bottom + 5}px`; tooltipEl.style.left = `${window.scrollX + rect.left}px`; } function removeTooltip() { if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } } function handleInlineToggle(trigger) { removeTooltip(); const myId = trigger.dataset.targetId; const ids = JSON.parse(trigger.dataset.postIds); const parentItem = trigger.closest('.quote-preview-item'); if (parentItem) { let wrapper = parentItem.querySelector('.quote-sub-wrapper'); if (wrapper) { if (wrapper.style.display === 'none') { wrapper.style.display = 'block'; trigger.textContent = trigger.textContent.replace(' ▼', '') + ' ▼'; } else { wrapper.style.display = 'none'; trigger.textContent = trigger.textContent.replace(' ▼', ''); } } else { wrapper = document.createElement('div'); wrapper.className = 'quote-wrapper-box quote-sub-wrapper'; wrapper.innerHTML = buildHeaderHTML(ids.length) + `
${buildPreviewHTML(ids, true)}
`; parentItem.appendChild(wrapper); trigger.textContent = trigger.textContent + ' ▼'; } } else { const wrapperId = `inline-wrapper-${myId}`; let wrapper = document.getElementById(wrapperId); if (wrapper) { if (wrapper.style.display === 'none') { wrapper.querySelector('.inline-list-container').innerHTML = buildPreviewHTML(ids, true); wrapper.style.display = 'block'; trigger.textContent = trigger.textContent.replace(' ▼', '') + ' ▼'; } else { wrapper.style.display = 'none'; trigger.textContent = trigger.textContent.replace(' ▼', ''); } } else { wrapper = document.createElement('div'); wrapper.id = wrapperId; wrapper.className = 'quote-wrapper-box'; wrapper.style.cssText = ` margin-top: 5px; padding: 8px; border: 1px dashed ${CONFIG.borderColor}; background: ${CONFIG.inlineBg}; border-radius: 4px; width: 96%; display: flow-root; `; wrapper.innerHTML = buildHeaderHTML(ids.length) + `
${buildPreviewHTML(ids, true)}
`; const parentTd = trigger.closest('td') || trigger.parentElement; parentTd.appendChild(wrapper); trigger.textContent = trigger.textContent + ' ▼'; } } } function buildHeaderHTML(count) { return `
引用レス (${count}件)
`; } function showImagePreview(imgNode) { if (imgPreviewEl) imgPreviewEl.remove(); const parentLink = imgNode.closest('a'); const highResSrc = parentLink ? parentLink.href : imgNode.src; imgPreviewEl = document.createElement('img'); imgPreviewEl.src = highResSrc; imgPreviewEl.style.cssText = ` position: fixed; z-index: 10001; pointer-events: none; max-width: ${CONFIG.previewImgSize}px; max-height: ${CONFIG.previewImgSize}px; border: 2px solid #333; background: #fff; box-shadow: 4px 4px 12px rgba(0,0,0,0.5); `; document.body.appendChild(imgPreviewEl); const rect = imgNode.getBoundingClientRect(); imgPreviewEl.style.top = `${rect.top}px`; imgPreviewEl.style.left = `${rect.right + 10}px`; } function hideImagePreview() { if (imgPreviewEl) { imgPreviewEl.remove(); imgPreviewEl = null; } } function buildPreviewHTML(ids, clickable) { let html = ''; ids.forEach(id => { const el = document.getElementById(`resContainer-${id}`); if (!el) return; const blockquote = el.querySelector('blockquote'); const quoteHTML = blockquote ? blockquote.innerHTML : '(本文なし)'; let imgHTML = ''; let hasImageClass = ''; const originalImg = el.querySelector('img'); if (originalImg) { hasImageClass = 'has-image'; const parentLink = originalImg.closest('a'); const href = parentLink ? parentLink.href : originalImg.src; imgHTML = ``; } const linkTag = clickable ? `>>No.${id}` : `>>No.${id}`; let grandChildLink = ''; if (quoteMap[id] && quoteMap[id].length > 0) { const grandChildIds = quoteMap[id]; const jsonIds = JSON.stringify(grandChildIds); grandChildLink = `(被引用:${grandChildIds.length})`; } html += `
${imgHTML}
${linkTag} ${grandChildLink} ${quoteHTML}
`; }); return html; } })();