NodeSeek & DeepFlood 双边会晤

在NodeSeek和DeepFlood之间阅读对方站点的帖子

安装此脚本
作者推荐脚本

您可能也喜欢NodeSeek X

安装此脚本
// ==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);
})();