// ==UserScript==
// @name NodeSeek & DeepFlood 双边会晤
// @namespace http://www.nodeseek.com/
// @version 1.0.2
// @description 在NodeSeek和DeepFlood之间阅读对方站点的帖子
// @author dabao
// @match *://www.nodeseek.com/*
// @match *://www.deepflood.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @run-at document-end
// @license GPL-3.0 License
// ==/UserScript==
(function() {
'use strict';
const path = window.location.pathname + window.location.search; // 包含 ?query
// 定义允许的 path 前缀/模式
const allowedPatterns = [
/^\/$/,
/^\?sortBy=/,
/^\/page-\d+/,
/^\/post-\d+/,
/^\/categories\//,
/^\/award/
];
const matched = allowedPatterns.some(re => re.test(path));
if (!matched) return; // 不匹配则不执行
const currentHost = window.location.hostname;
const targetSite = currentHost === 'www.nodeseek.com' ?
{ name: 'DeepFlood', url: 'https://www.deepflood.com' } :
{ name: 'NodeSeek', url: 'https://www.nodeseek.com' };
GM_addStyle(`
#dual-site-modal { position: fixed; top: 40px; right: 0; bottom: 0; width: 400px; background: #fff; border: 1px solid #ddd; border-radius: 8px 0 0 0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); opacity: 0.6;transition: opacity 0.3s ease; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
#dual-site-modal:hover { opacity: 1; }
#dual-site-header { padding: 8px 16px; background: #f8f9fa; border-bottom: 1px solid #eee; border-radius: 8px 0 0 0; display: flex; justify-content: space-between; align-items: center; }
#dual-site-title { font-weight: 600; font-size: 14px; color: #333; margin: 0; }
#dual-site-refresh:disabled { background: #6c757d; cursor: not-allowed; }
#dual-site-iframe { width: 100%; height: calc(100% - 49px); border: none; overflow: hidden; }
#dual-site-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; font-size: 14px; }
#fast-nav-button-group { right: calc(50% - 540px) !important; }
`);
function createModal() {
const modal = document.createElement('div');
modal.id = 'dual-site-modal';
modal.innerHTML = `
<div id="dual-site-header">
<h3 id="dual-site-title"><a href="${targetSite.url}" target="_blank">${targetSite.name}</a></h3>
<button id="dual-site-refresh" class="btn">刷新</button>
</div>
<iframe id="dual-site-iframe" style="display:none"></iframe>
<div id="dual-site-loading">加载中...</div>
`;
document.body.appendChild(modal);
const iframe = modal.querySelector('#dual-site-iframe');
const loading = modal.querySelector('#dual-site-loading');
const refreshBtn = modal.querySelector('#dual-site-refresh');
refreshBtn.addEventListener('click', loadData);
return { iframe, loading, refreshBtn };
}
function setLinksTarget(doc) {
try {
doc.querySelectorAll('a[href]').forEach(a => a.target = '_blank');
} catch (e) {
console.error('无法修改 iframe 内部链接:', e);
}
}
function attachSorterHandlers(docIframe) {
const sorter = docIframe.querySelector('div.sorter');
if (!sorter) return;
sorter.querySelectorAll('a[data-sort]').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
e.stopImmediatePropagation();
const sortBy = a.dataset.sort;
loadData(sortBy);
}, true);
});
}
function loadData(sortBy) {
const { iframe, loading, refreshBtn } = elements;
loading.style.display = 'block';
iframe.style.display = 'none';
refreshBtn.disabled = true;
refreshBtn.textContent = '加载中...';
const initUrl = `${targetSite.url}${sortBy?.trim() ? `?sortBy=${sortBy}` : ''}`;
GM_xmlhttpRequest({
method: 'GET',
url: initUrl,
onload: function(res) {
const parser = new DOMParser();
const doc = parser.parseFromString(res.responseText, "text/html");
// 插入 <base>,保证相对路径资源能正确加载
const base = doc.createElement("base");
base.href = `${targetSite.url}/`;
const head = doc.querySelector("head") || doc.documentElement;
head.insertBefore(base, head.firstChild);
// 移除头尾及左右侧栏
doc.querySelectorAll("body > header, body > footer, #nsk-left-panel-container, #nsk-right-panel-container").forEach(e => e.remove());
const htmlStr = '<!DOCTYPE html>\n' + doc.documentElement.outerHTML;
const blob = new Blob([htmlStr], { type: "text/html" });
const blobUrl = URL.createObjectURL(blob);
iframe.src = blobUrl;
iframe.onload = () => {
loading.style.display = 'none';
iframe.style.display = 'block';
refreshBtn.disabled = false;
refreshBtn.textContent = '刷新';
// 接管内部超链接
setLinksTarget(iframe.contentDocument);
attachSorterHandlers(iframe.contentDocument);
// ✅ 分页逻辑
const doc = iframe.contentDocument;
const postList = doc.querySelector('ul.post-list');
const topPager = doc.querySelector('div.nsk-pager.pager-top');
const bottomPager = doc.querySelector('div.nsk-pager.pager-bottom');
if (!postList) return;
const state = { page: 2, isLoading: false };
const loadMore = async () => {
if (state.isLoading) return;
state.isLoading = true;
GM_xmlhttpRequest({
method: 'GET',
url: `${targetSite.url}/page-${state.page}`,
onload: function(res) {
const parser = new DOMParser();
const newDoc = parser.parseFromString(res.responseText, "text/html");
setLinksTarget(newDoc);
const newList = newDoc.querySelector('ul.post-list');
const newTopPager = newDoc.querySelector('div.nsk-pager.pager-top');
const newBottomPager = newDoc.querySelector('div.nsk-pager.pager-bottom');
if (newList) {
newList.querySelectorAll('li').forEach(li => postList.appendChild(li));
topPager.innerHTML = newTopPager.innerHTML;
bottomPager.innerHTML = newBottomPager.innerHTML;
state.page++;
}
state.isLoading = false;
},
onerror: () => {
console.error("分页加载失败");
state.isLoading = false;
}
});
};
const checkShouldLoad = () => {
const { scrollTop, clientHeight, scrollHeight } = doc.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 690) {
loadMore();
}
};
const throttle = (fn, delay) => {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn(...args);
}
};
};
const debounced = (() => {
let timer;
return (fn, delay) => (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
})();
const throttledCheck = throttle(checkShouldLoad, 200);
const debouncedCheck = debounced(checkShouldLoad, 300);
doc.addEventListener('scroll', () => {
throttledCheck();
debouncedCheck();
});
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
};
},
onerror: () => {
loading.innerHTML = '<span style="color: #dc3545;">加载失败</span>';
refreshBtn.disabled = false;
refreshBtn.textContent = '重试';
}
});
}
const elements = createModal();
setTimeout(loadData, 1000);
})();