NodeSeek SidePeek

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();