NodeSeek SidePeek

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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();
})();