NodeSeek SidePeek

Preview NodeSeek topics in a floating right drawer. Original page remains interactive and no blur.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NodeSeek SidePeek
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Preview NodeSeek topics in a floating right drawer. Original page remains interactive and no blur.
// @author       NodeSeek SidePeek
// @match        https://www.nodeseek.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    const CONFIG = {
        drawerWidth: "clamp(360px, 42vw, 920px)",
        drawerMode: "overlay",   // 浮层模式,不挤压原页面
        iframeMode: true,
    };

    let drawerRoot = null;
    let iframe = null;
    let resizeHandle = null;
    let activeLink = null;
    let isResizing = false;
    let isDrawerOpen = false;

    // 获取所有帖子链接
    function getPostLinks() {
        const links = [];
        const items = document.querySelectorAll('.post-list-item .post-title a');
        items.forEach(link => {
            if (link instanceof HTMLAnchorElement && link.href && !link.closest('#ns-drawer-root')) {
                links.push(link);
            }
        });
        return links;
    }

    function isPostLink(link) {
        if (!(link instanceof HTMLAnchorElement)) return false;
        return /\/post-\d+-\d+/.test(link.pathname);
    }

    // 打开或更新侧边栏内容
    function openOrUpdateDrawer(url, linkElement) {
        if (!drawerRoot) createDrawer();

        iframe.src = url;

        if (activeLink) activeLink.classList.remove('ns-drawer-active-link');
        activeLink = linkElement;
        activeLink.classList.add('ns-drawer-active-link');

        if (!isDrawerOpen) {
            document.body.classList.add('ns-drawer-open');
            drawerRoot.style.transform = 'translateX(0)';
            isDrawerOpen = true;
        }
        document.body.style.paddingRight = '';
    }

    function closeDrawer() {
        if (!drawerRoot) return;
        document.body.classList.remove('ns-drawer-open');
        drawerRoot.style.transform = 'translateX(100%)';
        isDrawerOpen = false;
        if (activeLink) {
            activeLink.classList.remove('ns-drawer-active-link');
            activeLink = null;
        }
    }

    function createDrawer() {
        if (drawerRoot) return;
        drawerRoot = document.createElement('aside');
        drawerRoot.id = 'ns-drawer-root';
        drawerRoot.setAttribute('aria-hidden', 'true');
        drawerRoot.innerHTML = `
            <div class="ns-drawer-resize-handle" title="拖动调整宽度"></div>
            <div class="ns-drawer-shell">
                <div class="ns-drawer-header">
                    <div class="ns-drawer-title-group">
                        <div class="ns-drawer-eyebrow">NodeSeek 预览</div>
                        <h2 class="ns-drawer-title">点击帖子标题开始预览</h2>
                    </div>
                    <div class="ns-drawer-actions">
                        <button class="ns-drawer-close" title="关闭抽屉 (Esc)">✕</button>
                    </div>
                </div>
                <div class="ns-drawer-body">
                    <iframe class="ns-drawer-iframe" src="about:blank" title="帖子预览"></iframe>
                </div>
            </div>
        `;
        document.body.appendChild(drawerRoot);

        iframe = drawerRoot.querySelector('.ns-drawer-iframe');
        const closeBtn = drawerRoot.querySelector('.ns-drawer-close');
        resizeHandle = drawerRoot.querySelector('.ns-drawer-resize-handle');

        closeBtn.addEventListener('click', closeDrawer);

        if (resizeHandle) {
            resizeHandle.addEventListener('mousedown', startResize);
        }

        document.documentElement.style.setProperty('--ns-drawer-width', CONFIG.drawerWidth);
        document.body.classList.add('ns-drawer-mode-overlay');
    }

    function startResize(e) {
        if (e.button !== 0) return;
        e.preventDefault();
        isResizing = true;
        document.body.classList.add('ns-drawer-resizing');
        document.addEventListener('mousemove', onResizeMove);
        document.addEventListener('mouseup', stopResize);
    }

    function onResizeMove(e) {
        if (!isResizing) return;
        let newWidth = window.innerWidth - e.clientX;
        const minWidth = 320;
        const maxWidth = Math.min(1400, window.innerWidth - 40);
        newWidth = Math.min(maxWidth, Math.max(minWidth, newWidth));
        document.documentElement.style.setProperty('--ns-drawer-width', `${newWidth}px`);
    }

    function stopResize() {
        isResizing = false;
        document.body.classList.remove('ns-drawer-resizing');
        document.removeEventListener('mousemove', onResizeMove);
        document.removeEventListener('mouseup', stopResize);
    }

    // 监听动态加载的链接
    function observeLinks() {
        const observer = new MutationObserver(() => {
            attachLinkListeners();
        });
        observer.observe(document.body, { childList: true, subtree: true });
        attachLinkListeners();
    }

    function attachLinkListeners() {
        const links = getPostLinks();
        for (const link of links) {
            if (link.dataset.nsPreviewAttached) continue;
            link.dataset.nsPreviewAttached = 'true';
            link.addEventListener('click', (e) => {
                if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey) return;
                const url = link.href;
                if (isPostLink(link) && url) {
                    e.preventDefault();
                    e.stopPropagation();
                    openOrUpdateDrawer(url, link);
                }
            });
        }
    }

    // 点击外部区域关闭侧边栏(但不关闭更新链接时触发的点击)
    function setupOutsideClick() {
        document.addEventListener('click', (e) => {
            if (!isDrawerOpen) return;
            // 如果点击目标在抽屉内部,不关闭
            if (drawerRoot && drawerRoot.contains(e.target)) return;
            // 如果点击目标是帖子链接(且该链接已绑定预览行为),不关闭,因为 openOrUpdateDrawer 会处理更新
            let target = e.target;
            while (target && target !== document) {
                if (target.tagName === 'A' && target.href && target.closest('.post-list-item .post-title a')) {
                    // 是帖子链接,交给链接自己的处理器,不关闭
                    return;
                }
                target = target.parentElement;
            }
            closeDrawer();
        });
    }

    // 注入样式:无遮罩、无模糊
    function injectStyles() {
        GM_addStyle(`
            :root {
                --ns-drawer-width: clamp(360px, 42vw, 920px);
            }
            body.ns-drawer-resizing,
            body.ns-drawer-resizing * {
                cursor: ew-resize !important;
                user-select: none !important;
            }
            #ns-drawer-root {
                position: fixed;
                top: 0;
                right: 0;
                bottom: 0;
                width: var(--ns-drawer-width);
                z-index: 2147483647;
                transform: translateX(100%);
                transition: transform 0.2s ease;
                pointer-events: none;
            }
            body.ns-drawer-open #ns-drawer-root {
                transform: translateX(0);
            }
            #ns-drawer-root .ns-drawer-resize-handle {
                position: absolute;
                left: 0;
                top: 0;
                bottom: 0;
                width: 8px;
                transform: translateX(-50%);
                cursor: ew-resize;
                pointer-events: auto;
                z-index: 10;
            }
            #ns-drawer-root .ns-drawer-resize-handle:hover {
                background: rgba(0, 0, 0, 0.1);
            }
            #ns-drawer-root .ns-drawer-shell {
                position: relative;
                height: 100%;
                display: flex;
                flex-direction: column;
                background: var(--bg-main-color, #ffffff);
                border-left: 1px solid rgba(0, 0, 0, 0.1);
                box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15);
                pointer-events: auto;
            }
            #ns-drawer-root .ns-drawer-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 16px 20px;
                border-bottom: 1px solid rgba(0, 0, 0, 0.08);
                background: inherit;
                flex-shrink: 0;
            }
            #ns-drawer-root .ns-drawer-title-group {
                flex: 1;
                min-width: 0;
            }
            #ns-drawer-root .ns-drawer-eyebrow {
                font-size: 12px;
                color: #888;
                margin-bottom: 4px;
            }
            #ns-drawer-root .ns-drawer-title {
                margin: 0;
                font-size: 18px;
                font-weight: 600;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            #ns-drawer-root .ns-drawer-actions {
                margin-left: 12px;
            }
            #ns-drawer-root .ns-drawer-close {
                background: transparent;
                border: none;
                font-size: 24px;
                line-height: 1;
                cursor: pointer;
                color: #666;
                padding: 0 8px;
                border-radius: 6px;
            }
            #ns-drawer-root .ns-drawer-close:hover {
                background: rgba(0, 0, 0, 0.05);
                color: #000;
            }
            #ns-drawer-root .ns-drawer-body {
                flex: 1;
                min-height: 0;
                overflow: auto;
            }
            #ns-drawer-root .ns-drawer-iframe {
                width: 100%;
                height: 100%;
                border: none;
                display: block;
            }
            .ns-drawer-active-link {
                color: #f90 !important;
                font-weight: bold;
                text-decoration: underline;
            }
            @media (max-width: 720px) {
                #ns-drawer-root {
                    width: 100vw;
                }
                .ns-drawer-resize-handle {
                    display: none;
                }
            }
        `);
    }

    function init() {
        injectStyles();
        createDrawer();
        observeLinks();
        setupOutsideClick();
    }

    init();
})();