NodeSeek SidePeek

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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