YouTube Enhancer (Simple)

YouTube 增强:极简单图预览、D键开关、修复频道名、自动跳过 Shorts、J/K 导航、空格新标签页。

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         YouTube Enhancer (Simple)
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  YouTube 增强:极简单图预览、D键开关、修复频道名、自动跳过 Shorts、J/K 导航、空格新标签页。
// @author       Antigravity
// @match        *://www.youtube.com
// @grant        GM_download
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const SITE_CONFIG = {
        name: "YouTube",
        selectors: {
            item: 'ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer',
            author: '#channel-name #text, ytd-channel-name #text, .yt-lockup-metadata-view-model__metadata a[href*="/@"], #byline-container a',
            postLink: 'a#video-title-link, a#video-title, .yt-lockup-metadata-view-model__title, .yt-lockup-view-model__content-image',
            contentSpans: '#video-title, #video-title-link, .yt-lockup-metadata-view-model__title',
            media: { img: 'yt-image img, .yt-core-image, img' }
        },
        theme: { accent: '#ff0000', overlayBg: 'rgba(0,0,0,0.96)', borderRadius: '16px' }
    };

    let policy = { createHTML: (s) => s };
    if (window.trustedTypes?.createPolicy) {
        try { policy = window.trustedTypes.createPolicy('enhancer-policy', { createHTML: (s) => s }); } catch (e) { }
    }
    const setHTML = (el, html) => {
        if (!el) return;
        try { el.innerHTML = policy.createHTML(html); } catch (e) { el.textContent = html.replace(/<[^>]*>/g, ''); }
    };

    const App_Core = (() => {
        return {
            getPostData: (card) => {
                if (!card) return null;
                const linkEl = card.querySelector(SITE_CONFIG.selectors.postLink);
                const titleEl = card.querySelector(SITE_CONFIG.selectors.contentSpans);
                const authorEl = card.querySelector(SITE_CONFIG.selectors.author);
                const avatarImg = card.querySelector('yt-img-shadow#avatar img, #avatar img, .yt-spec-avatar-shape__image');
                const mainImg = card.querySelector(SITE_CONFIG.selectors.media.img);

                return {
                    row: card,
                    url: linkEl?.href || null,
                    content: titleEl?.innerText.trim() || titleEl?.title || "",
                    author: authorEl?.title || authorEl?.innerText.trim() || "Channel",
                    avatarUrl: avatarImg?.src || avatarImg?.getAttribute('src'),
                    src: mainImg?.src || mainImg?.getAttribute('src')
                };
            },
            findComments: () => {
                const all = Array.from(document.querySelectorAll(SITE_CONFIG.selectors.item));
                return all.filter(c => {
                    // 核心过滤:排除嵌套关系,确保每个评论只抓取一次最外层
                    let p = c.parentElement;
                    while (p) { if (p.matches?.(SITE_CONFIG.selectors.item)) return false; p = p.parentElement; }
                    return c.offsetHeight > 20;
                });
            },
            findCards: () => {
                const all = Array.from(document.querySelectorAll(SITE_CONFIG.selectors.item));
                return all.filter(c => {
                    if (c.hasAttribute('is-slim-media') || c.querySelector('ytm-shorts-lockup-view-model')) return false;
                    const link = c.querySelector(SITE_CONFIG.selectors.postLink);
                    if (link && link.href && link.href.includes('/shorts/')) return false;

                    // 核心过滤:排除嵌套关系
                    let p = c.parentElement;
                    while (p) { if (p.matches?.(SITE_CONFIG.selectors.item)) return false; p = p.parentElement; }

                    return c.offsetHeight > 50;
                });
            }
        };
    })();

    const App_UI = (() => {
        let overlay, vImg, vText, vInfo, vAvatar;
        let isEnabled = true;

        const injectCSS = () => {
            if (document.getElementById('enhancer-css')) return;
            const s = document.createElement('style'); s.id = 'enhancer-css';
            s.textContent = `
                .eh-active { outline: 4px solid ${SITE_CONFIG.theme.accent} !important; outline-offset: 0px !important; border-radius: 12px !important; z-index: 10 !important; position: relative !important; }
                .eh-main-image { width: 100%; height: auto; object-fit: contain; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
                .eh-avatar { width: 60px; height: 60px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.2); object-fit: cover; }
                .eh-author { font-size: 24px; font-weight: 900; color: #fff; }
                .eh-title { font-size: 26px; font-weight: 700; line-height: 1.4; color: rgba(255,255,255,0.9); margin-top: 10px; }
            `;
            document.head.appendChild(s);
        };

        const update = (data, idx, total) => {
            if (!data) return;
            vImg.src = data.src || '';
            // 头部:头像 + 作者 (水平排列)
            setHTML(vAvatar, `
                <div style="display:flex; align-items:center; gap:20px; margin-bottom:15px;">
                    <img src="${data.avatarUrl || ''}" class="eh-avatar">
                    <div class="eh-author">${data.author}</div>
                </div>
            `);
            // 视频标题
            setHTML(vText, `<div class="eh-title">${data.content}</div>`);

            // 底部布局:左(链接) 中(提示) 右(滑块)
            setHTML(vInfo, `
                <div style="border-top:1px solid rgba(255,255,255,0.1); padding-top:15px; margin-top:10px; display:flex; align-items:center; justify-content:space-between; width:100%;">
                    <!-- 左下:更新地址 -->
                    <div style="flex:1; text-align:left;">
                        <a href="https://greasyfork.org/zh-CN/scripts/564767" target="_blank" style="color:rgba(255,255,255,0.2); font-size:10px; text-decoration:none; transition:color 0.2s;" onmouseover="this.style.color='rgba(255,255,255,0.5)'" onmouseout="this.style.color='rgba(255,255,255,0.2)'">GreasyFork / Info</a>
                    </div>

                    <!-- 中间:提示 -->
                    <div style="flex:1.5; text-align:center; color:rgba(255,255,255,0.4); font-size:11px; font-weight:500; display:flex; align-items:center; justify-content:center; gap:20px;">
                        <span>Pro Navigation Enabled</span>
                        <span><b>[SPACE]</b> 新标签页</span>
                        <span><b>[D]</b> 关闭</span>
                    </div>

                    <!-- 右下:卡片宽度 -->
                    <div style="flex:1; display:flex; align-items:center; justify-content:flex-end; gap:10px;">
                        <span style="font-size:10px; color:rgba(255,255,255,0.3); white-space:nowrap;">宽度</span>
                        <input type="range" id="eh-size-slider" min="400" max="1400" step="50" value="${GM_getValue('eh_card_width', 800)}" style="width:80px; cursor:pointer; accent-color:#ffffff00;">
                    </div>
                </div>
            `);
            // counter 已移除

            // 重新绑定宽度调节和设置
            const slider = document.getElementById('eh-size-slider');
            const card = vImg.parentElement;

            if (slider && card) {
                const updateWidth = (val) => { card.style.width = `${val}px`; GM_setValue('eh_card_width', val); };
                slider.oninput = (e) => updateWidth(e.target.value);
                updateWidth(slider.value);
            }
        };

        return {
            show: (data, idx, total) => {
                if (!isEnabled) return;
                if (!overlay) {
                    overlay = document.createElement('div');
                    Object.assign(overlay.style, {
                        position: 'fixed',
                        inset: 0,
                        backgroundColor: 'rgba(0,0,0,0.85)',
                        zIndex: 999999,
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                    });
                    const card = document.createElement('div');
                    Object.assign(card.style, {
                        width: GM_getValue('eh_card_width', 800) + 'px',
                        maxWidth: '90vw',
                        maxHeight: '90vh',
                        background: 'rgba(255,255,255,0.05)',
                        backdropFilter: 'blur(20px) saturate(180%)',
                        borderRadius: '30px',
                        padding: '40px',
                        position: 'relative',
                        boxShadow: '0 40px 100px rgba(0,0,0,0.8)',
                        border: '1px solid rgba(255,255,255,0.1)',
                        display: 'flex',
                        flexDirection: 'column',
                        overflowX: 'hidden',
                        overflowY: 'auto'
                    });

                    vImg = document.createElement('img'); vImg.className = 'eh-main-image';
                    vAvatar = document.createElement('div');
                    vText = document.createElement('div');
                    vInfo = document.createElement('div');

                    card.append(vImg, vAvatar, vText, vInfo); overlay.append(card); document.body.appendChild(overlay);
                    overlay.onclick = (e) => { if (e.target === overlay) overlay.style.display = 'none'; };
                }
                overlay.style.display = 'flex';
                update(data, idx, total);
            },
            hide: () => { if (overlay) overlay.style.display = 'none'; },
            toggle: () => { isEnabled = !isEnabled; if (!isEnabled) App_UI.hide(); App_UI.showToast(isEnabled ? '预览:开启' : '预览:关闭'); return isEnabled; },
            isVisible: () => overlay && overlay.style.display === 'flex' && isEnabled,
            injectCSS,
            showToast: (msg) => {
                const old = document.getElementById('enhancer-toast'); if (old) old.remove();
                const t = document.createElement('div'); t.id = 'enhancer-toast';
                Object.assign(t.style, { position: 'fixed', bottom: '50px', left: '50%', transform: 'translateX(-50%)', padding: '12px 30px', background: '#fff', color: '#000', borderRadius: '30px', zIndex: 1000005, fontWeight: '900', boxShadow: '0 15px 30px rgba(0,0,0,0.4)' });
                t.innerText = msg; document.body.appendChild(t);
                setTimeout(() => { t.style.opacity = '0'; t.style.transition = '0.3s'; setTimeout(() => t.remove(), 500); }, 1500);
            }
        };
    })();

    const App_Nav = (() => {
        let curIdx = -1, cards = [];
        return {
            move: (direction) => {
                cards = App_Core.findCards();
                if (!cards.length) return null;

                // 首次按下,寻找中心卡片
                if (curIdx === -1 || !cards[curIdx]) {
                    const centerTop = window.innerHeight / 2;
                    const sorted = [...cards].sort((a, b) => {
                        const ra = a.getBoundingClientRect();
                        const rb = b.getBoundingClientRect();
                        return Math.abs(ra.top + ra.height / 2 - centerTop) - Math.abs(rb.top + rb.height / 2 - centerTop);
                    });
                    curIdx = cards.indexOf(sorted[0]);
                    return { data: App_Core.getPostData(cards[curIdx]), index: curIdx, total: cards.length };
                }

                if (direction === 'left') {
                    curIdx = Math.max(0, curIdx - 1);
                } else if (direction === 'right') {
                    curIdx = Math.min(cards.length - 1, curIdx + 1);
                } else {
                    // 上下移动:空间几何查找
                    const rect = cards[curIdx].getBoundingClientRect();
                    const curX = rect.left + rect.width / 2;
                    let bestIdx = -1;
                    let minDiff = Infinity;

                    for (let i = 0; i < cards.length; i++) {
                        if (i === curIdx) continue;
                        const r = cards[i].getBoundingClientRect();
                        const targetX = r.left + r.width / 2;

                        const isBelow = r.top > rect.top + 10;
                        const isAbove = r.bottom < rect.bottom - 10;

                        if ((direction === 'down' && isBelow) || (direction === 'up' && isAbove)) {
                            const dy = Math.abs(r.top - rect.top);
                            const dx = Math.abs(targetX - curX);
                            const score = dy + dx * 2.8; // YouTube 权重稍微调高,确保垂直对齐

                            if (score < minDiff) {
                                minDiff = score;
                                bestIdx = i;
                            }
                        }
                    }
                    if (bestIdx !== -1) curIdx = bestIdx;
                }
                return { data: App_Core.getPostData(cards[curIdx]), index: curIdx, total: cards.length };
            },
            scroll: () => {
                document.querySelectorAll('.eh-active').forEach(e => e.classList.remove('eh-active'));
                const c = cards[curIdx];
                if (c) { c.classList.add('eh-active'); c.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
            },
            reset: () => { curIdx = -1; document.querySelectorAll('.eh-active').forEach(e => e.classList.remove('eh-active')); },
            getCard: () => cards[curIdx]
        };
    })();

    const init = () => {
        window.addEventListener('keydown', (e) => {
            if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.hasAttribute('contenteditable') || document.activeElement.id === 'search') return;
            const k = e.key.toLowerCase();
            if (k === 'd') {
                const enabled = App_UI.toggle();
                if (enabled) { const r = App_Nav.move(0); if (r) App_UI.show(r.data, r.index, r.total); }
                return;
            }

            // 导航逻辑
            let dir = '';
            const isVisible = App_UI.isVisible();

            if (k === 'j' || k === 'arrowdown') {
                // 如果预览开着,简单的索引加 1 往往符合直觉,但为了统一,全部走空间导航
                dir = 'down';
            } else if (k === 'k' || k === 'arrowup') {
                dir = 'up';
            } else if (k === 'l' || k === 'arrowright') {
                dir = 'right';
            } else if (k === 'h' || k === 'arrowleft') {
                dir = 'left';
            }

            if (dir) {
                e.preventDefault();
                const r = App_Nav.move(dir);
                if (r) {
                    if (isVisible) App_UI.show(r.data, r.index, r.total);
                    App_Nav.scroll();
                }
                return;
            }

            if (e.code === 'Space') {
                if (App_UI.isVisible() || !window.location.pathname.startsWith('/watch')) {
                    e.preventDefault(); e.stopPropagation();
                    const card = App_Nav.getCard();
                    if (card) { const data = App_Core.getPostData(card); if (data?.url) window.open(data.url, '_blank'); }
                }
            }
            else if (k === 'escape' && App_UI.isVisible()) App_UI.hide();
        }, true);
        App_UI.injectCSS();
        let lp = window.location.href; setInterval(() => { if (window.location.href !== lp) { lp = window.location.href; App_Nav.reset(); App_UI.hide(); } }, 800);
    };
    setTimeout(init, 2000);
})();