// ==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) + `