NodeSeek SidePeek

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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